Avant de lire ceci, il est sans doute sage d'avoir lu et compris l'article sur les Mutex et les autoverrous, celui sur les Mutex portables et celui sur les section critiques portables. Le code qui suit serait enrichi par l'injection de support aux objets et aux méthodes volatiles, mais j'ai volontairement choisi de ne pas aller jusque là pour simplifier les exemples.
Les verrous RAII (comme l'autoverrou proposé dans las articles susmentionnés) constituent une technique simple et efficace pour obtenir une resource puis en automatiser la libération en toutes circonstances.
Par exemple, dans le code de la classe X proposé à droite, la méthode X::f() est susceptible de lever une exception (un Zut). Le code de f() nous importe peu. La méthode g_risque() est risquée du fait que, si f() devait lever une exception, le mutex m_ obtenu avant l'invocation ne serait pas libéré. La méthode g_complexe() fonctionne mais est très lourde. L'approche ressemble à ce qui serait nécessaire en Java, c'est-à-dire laisser le code client du m_ gérer tous les cas possibles sur une base manuelle (Java propose, heureusement, une clause finally aux séquences try...catch pour alléger cette tâche; C# offre un mécanisme particulier, les blocs using, qui fonctionnent si le clode client implémente un idiome local à la plateforme .NET et reposant sur une interface nommée IDisposable). En pratique, le code client complexe mène à des programmes difficiles à écrire et à entretenir. En retour, le recours à un objet RAII comme l'Autoverrou nommé av dans la méthode g_securitaire() assure la libération en tout temps du mutex m_ de par l'invocation automatique et déterministe du destructeur d'av, que la méthode g_securitaire() se complète normalement (atteinte de la fin de sa portée) ou anormalement (levée d'une exception par f()). |
|
Ces mécanismes fonctionnement bien en général. Les outils de synchronisation (ici : le mutex) sont acquis et libérés proprement (à moins de cas pathologiques comme une inversion de priorités) et le code client de l'outil de synchronisation demeure relativement simple à écrire et à utiliser.
En pratique, cependant, il peut s'agir d'un mécanisme trop rigide pour certains types de systèmes, en particulier les systèmes en temps réel pour lesquels une mise en attente pour une durée indéterministe (ce que fait Autoverrou ici : une tentative d'obtention arbitrairement longue de la ressource) invalide, en pratique, le système dans son ensemble – la validité d'un système temps réel, après tout, dépend de sa capacité de livrer les bosn résultats et de les livrer dans un temps maximal connu au préalable.
Dans de telles situations, il faut raffiner le modèle est penser un outil RAII qui :
Nous donnerons un exemple d'un tel outil en exprimant un Try-Lock RAII à partir de modifications sur Autoverrou, puis nous examinerons les modifications que cela entraînera sur la classe X posée en exemple plus haut. L'approche sera la même pour d'autres outils de synchronisation (un Try-Enter sur une section critique suivra le même modèle).
// ... classe Mutex ...
class try_lock
: Incopiable
{
const Mutex &m_;
bool obtenu_;
public:
try_lock(const Mutex &m) noexcept
: m_{m}
{
obtenu_ = m_.obtenir(0);
}
~try_lock() noexcept
{ if (*this) m_.relacher(); }
operator bool() const noexcept
{ return obtenu_; }
};
L'idée d'un verrou potentiel et testable tel qu'un Try-Lock est simple :
Visiblement, l'adaptation de l'autoverrou classique à un modèle testable et plus fluide n'implique que des changements mineurs. Notez par contre que le try_lock, un peu comme les exceptions, ne peut que signaler un problème d'obtention, pas le traiter – réagir à une non-obtention de ressource est un problème qui ne connaît pas de solution générale et ind.épendante du contexte.
Pour que le try_lock soit utile, il faut que le code client soit adapté pour tenir compte du caractère incertain de l'obtention de la ressource sous sa gouverne. Ceci implique nécessairement quelques transformations. Si la non-obtention est une chose rare, alors une stratégie reposant sur la levée d'exceptions est envisageable. Dans l'exemple à droite, la méthode g_except() procède ainsi. L'immense avantage de cette approche est qu'elle ne transforme essentiellement pas l'interface exposée au code client; l'inconvénient est que le traitement d'exceptions ralentit l'exécution du code client, ce qui rend cette approche moins utile qu'il n'y paraît pour les systèmes à haute performance. Si la non-obtention est une chose assez fréquente pour mériter un traitment digne de ce nom, alors il est possible que transformer la méthode pour qu'elle retourne un signal de réussite ou d'échec soit préférable. La méthode g_return(), à droite, est un autre exemple d'une telle stratégie. Dans un tel cas, il est possible que la méthode originale doivent être dénaturée, surtout si elle retournait déjà quelque chose (il faut alors utiliser un paramètre par référence et modifier le code client pour accommoder la modification à la stratégie de synchronisation). Par exemple, une méthode int f(); deviendrait bool f(int&); si elle devait être augmentée d'un code de succès ou d'échec minimaliste. Cela affecterait alors le code client qui perdrait un peu de son naturel. |
|
L'idéal est d'utiliser les Try-Locks dans un contexte où la non-obtention de la ressource risque de ralentir les opérations sans nécessairement dénaturer la fonction qui les sollicite.
Examinez par exemple le code proposé à droite. Dans la fonction f(), la variable globale fdt se fait ajouter de manière successive des chaînes de caractères jusqu'à concurrence de FileDeTexte::MAX caractères. On présume ici que d'autres threads opèrent sur fdt aussi pourt justifier le recours au verrou. Remarquez la sdtructure de FileDeTexte::ajouter(). Cette méthode retourne le lieu de la fin de la dernière lecture (fin exclue). Par cons.quent, une non-consommation de données ou une consommation de grande taille de données apparaîtront sous la même forme pour le code client. Voilà un joli cas où le recours à des verrous testables n'entraîne pas de transformation du code client et améliore simplement la qualité du code dans son ensemble. Aucune perte, aucune transformation d'interface, mais un gain dse vitesse. Qui dit mieux? |
|