web-dev-qa-db-fra.com

Les mutex devraient-ils être mutables?

Je ne sais pas si c'est une question de style ou quelque chose qui a une règle stricte ...

Si je veux garder l'interface de méthode publique aussi constante que possible, mais sécuriser le thread objet, dois-je utiliser des mutex mutables? En général, est-ce un bon style, ou une interface de méthode non const devrait-elle être préférée? Veuillez justifier votre point de vue.

58
Marcin

[Réponse modifiée]

Fondamentalement, l'utilisation de méthodes const avec des mutex mutables est une bonne idée (ne renvoyez pas les références en passant, assurez-vous de renvoyer par valeur), au moins pour indiquer qu'elles ne modifient pas l'objet. Les mutex ne devraient pas être const, ce serait un mensonge éhonté de définir des méthodes de verrouillage/déverrouillage comme const ...

En fait, cela (et la mémorisation) sont les seules utilisations loyales que je vois du mot-clé mutable.

Vous pouvez également utiliser un mutex externe à votre objet: faites en sorte que toutes vos méthodes soient réentrantes et demandez à l'utilisateur de gérer lui-même le verrou: { lock locker(the_mutex); obj.foo(); } n'est pas si difficile à taper, et

{
    lock locker(the_mutex);
    obj.foo();
    obj.bar(42);
    ...
}

a l'avantage de ne pas nécessiter deux verrous mutex (et vous êtes assuré que l'état de l'objet n'a pas changé).

28
Alexandre C.

La question cachée est: où placez-vous le mutex protégeant votre classe?

En résumé, disons que vous voulez lire le contenu d'un objet protégé par un mutex.

La méthode "read" doit être sémantiquement "const" car elle ne modifie pas l'objet lui-même. Mais pour lire la valeur, vous devez verrouiller un mutex, extraire la valeur, puis déverrouiller le mutex, ce qui signifie que le mutex lui-même doit être modifié, ce qui signifie que le mutex lui-même ne peut pas être "const".

Si le mutex est externe

Alors tout va bien. L'objet peut être "const", et le mutex n'a pas besoin d'être:

Mutex mutex ;

int foo(const Object & object)
{
   Lock<Mutex> lock(mutex) ;
   return object.read() ;
}

À mon humble avis, c'est une mauvaise solution, car n'importe qui pourrait réutiliser le mutex pour protéger autre chose. En t'incluant. En fait, vous vous trahirez parce que, si votre code est suffisamment complexe, vous serez simplement confus quant à ce que tel ou tel mutex protège exactement.

Je sais: j'ai été victime de ce problème.

Si le mutex est interne

À des fins d'encapsulation, vous devez placer le mutex aussi près que possible de l'objet qu'il protège.

Habituellement, vous écrivez une classe avec un mutex à l'intérieur. Mais tôt ou tard, vous devrez protéger une structure STL complexe, ou toute autre chose écrite par un autre sans mutex à l'intérieur (ce qui est une bonne chose).

Une bonne façon de le faire est de dériver l'objet d'origine avec un modèle héritant en ajoutant la fonction mutex:

template <typename T>
class Mutexed : public T
{
   public :
      Mutexed() : T() {}
      // etc.

      void lock()   { this->m_mutex.lock() ; }
      void unlock() { this->m_mutex.unlock() ; } ;

   private :
      Mutex m_mutex ;
}

De cette façon, vous pouvez écrire:

int foo(const Mutexed<Object> & object)
{
   Lock<Mutexed<Object> > lock(object) ;
   return object.read() ;
}

Le problème est qu'il ne fonctionnera pas car object est const, et l'objet verrou appelle les méthodes non const lock et unlock.

Le dilemme

Si vous pensez que const est limité aux objets const au niveau du bit, alors vous êtes foutu et devez revenir à la "solution de mutex externe".

La solution consiste à admettre que const est davantage un qualificatif sémantique (tout comme volatile lorsqu'il est utilisé comme qualificateur de méthode de classes). Vous cachez le fait que la classe n'est pas entièrement const mais assurez-vous toujours de fournir une implémentation qui tient la promesse que les parties significatives de la classe ne seront pas modifiées lors de l'appel d'une méthode const.

Vous devez ensuite déclarer votre mutex mutable, et les méthodes de verrouillage/déverrouillage const:

template <typename T>
class Mutexed : public T
{
   public :
      Mutexed() : T() {}
      // etc.

      void lock()   const { this->m_mutex.lock() ; }
      void unlock() const { this->m_mutex.unlock() ; } ;

   private :
      mutable Mutex m_mutex ;
}

La solution mutex interne est une bonne solution à mon humble avis: avoir des objets déclarés l'un près de l'autre dans une main et les avoir agrégés dans un wrapper dans l'autre main, c'est la même chose à la fin.

Mais l'agrégation a les avantages suivants:

  1. C'est plus naturel (vous verrouillez l'objet avant d'y accéder)
  2. Un objet, un mutex. Comme le style de code vous oblige à suivre ce modèle, il diminue les risques de blocage car un mutex ne protégera qu'un seul objet (et pas plusieurs objets dont vous ne vous souviendrez pas vraiment), et un objet sera protégé par un seul mutex (et non par mutex multiple qui doit être verrouillé dans le bon ordre)
  3. La classe mutexée ci-dessus peut être utilisée pour n'importe quelle classe

Donc, gardez votre mutex aussi près que possible de l'objet mutex (par exemple en utilisant la construction Mutexed ci-dessus), et optez pour le qualificatif mutable pour le mutex.

Modifier 2013-01-04

Apparemment, Herb Sutter a le même point de vue: sa présentation sur les "nouvelles" significations de const et mutable en C++ 11 est très éclairante:

http://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

51
paercebal