std:: mutex vs std:: mutex récursif en tant que membre de la classe

j'ai vu certaines personnes détestent sur recursive_mutex:

http://www.zaval.org/resources/library/butenhof1.html

mais en réfléchissant à la façon d'implémenter une classe qui est sans fil (mutex protégé), il me semble extrêmement difficile de prouver que toute méthode qui devrait être mutex protégé est mutex protégé et que mutex est verrouillé au plus une fois.

ainsi pour la conception orientée objet, devrait std::recursive_mutex la valeur par défaut et std::mutex considéré comme une optimisation de la performance en général, sauf si elle n'est utilisée qu'en un seul endroit (pour protéger une seule ressource)?

pour clarifier les choses, je parle d'un mutex privé nonstatique. Chaque instance de classe n'a donc qu'un seul mutex.

Au début de chaque méthode publique:

{
    std::scoped_lock<std::recursive_mutex> sl;
38
demandé sur DJMcMayhem 2013-01-24 14:19:52

3 réponses

la plupart du temps, si vous pensez que vous avez besoin d'un mutex récursif, alors votre design est erroné, donc il ne devrait certainement pas être la valeur par défaut.

pour une classe avec un seul mutex protégeant les membres de données, alors le mutex doit être verrouillé dans tous les public fonctions de membre, et tous les private les fonctions de membre doivent supposer que le mutex est déjà verrouillé.

Si public fonction membre doit appeler un autre public fonction membre, puis diviser le second en deux:private fonction de mise en oeuvre qui fait le travail, et un public fonction de membre qui verrouille juste le mutex et appelle le private. La fonction first member peut alors également appeler la fonction implementation sans avoir à se soucier du verrouillage récursif.

e.g.

class X {
    std::mutex m;
    int data;
    int const max=50;

    void increment_data() {
        if (data >= max)
            throw std::runtime_error("too big");
        ++data;
    }
public:
    X():data(0){}
    int fetch_count() {
        std::lock_guard<std::mutex> guard(m);
        return data;
    }
    void increase_count() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
    } 
    int increase_count_and_return() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
        return data;
    } 
};

il s'agit bien sûr d'un exemple trivial artificiel, mais le increment_data la fonction est partagée entre deux fonctions publiques, dont chacune verrouille le mutex. En code simple fileté, il pourrait être incorporé dans increase_count et increase_count_and_return pourrait appeler cela, mais nous ne pouvons pas le faire en code multithread.

ce n'est qu'une application de principes de bonne conception: les fonctions de membre public prennent la responsabilité de verrouiller le mutex, et délèguent la responsabilité de faire le travail à la fonction de membre privé.

ceci a l'avantage que le public les fonctions de membre n'ont à faire face à être appelées que lorsque la classe est dans un état cohérent: le mutex est déverrouillé, et une fois qu'il est verrouillé alors tous les invariants tiennent. Si vous appelez public fonctions de membres les uns des autres alors ils doivent gérer le cas où le mutex est déjà verrouillé, et que les invariants ne tiennent pas nécessairement.

cela signifie aussi que des choses comme la variable de condition waits fonctionneront: si vous passez un verrou sur un mutex récursif à une variable de condition, alors (a) vous devez utiliser std::condition_variable_any parce que std::condition_variable ne fonctionnera pas, et (b) Un seul niveau de serrure est libéré, donc vous peut encore tenir la serrure, et donc l'impasse parce que le fil qui déclencherait le prédicat et faire la notification ne peut pas acquérir la serrure.

j'ai du mal à imaginer un scénario où un mutex récursif est nécessaire.

60
répondu Anthony Williams 2015-06-28 21:14:49

devrait std::recursive_mutex la valeur par défaut et std::mutex considéré comme une optimisation des performances?

pas vraiment, non. L'avantage d'utiliser des serrures non récursives est juste une optimisation des performances, cela signifie que votre code vérifie lui-même que les opérations atomiques au niveau de la feuille sont vraiment au niveau de la feuille, ils n'appellent pas quelque chose d'autre qui utilise la serrure.

il y a une situation assez courante où vous avez:

  • un fonction qui implémente une opération qui doit être sérialisée, donc il prend le mutex et le fait.
  • une autre fonction qui implémente une plus grande opération sérialisée, et qui veut appeler la première fonction pour en faire un pas, alors qu'elle tient la serrure pour l'opération plus grande.

pour un exemple concret, peut-être que la première fonction supprime atomiquement un noeud d'une liste, tandis que la seconde supprime atomiquement deux les noeuds d'une liste (et vous ne voulez jamais qu'un autre thread voit la liste avec seulement un des deux noeuds sortis).

Vous n'avez pas besoin mutex récursifs pour ceci. Par exemple, vous pouvez refactoriser la première fonction comme une fonction publique qui prend la serrure et appelle une fonction privée que l'opération "mal". La seconde fonction peut alors appeler la même fonction privée.

cependant, il est parfois commode d'utiliser un mutex récursif à la place. Il y a toujours un problème avec cette conception: remove_two_nodes appelle remove_one_node à un point où une classe invariant qui ne tient pas (la deuxième fois qu'il l'appelle, la liste est précisément dans l'état, nous ne voulons pas exposer). Mais en supposant que nous savons que remove_one_node ne s'appuie pas sur cet invariant ce n'est pas un défaut tueur dans la conception, c'est juste que nous avons rendu nos règles un peu plus complexes que l'idéal "tous les invariants de classe tiennent toujours chaque fois que n'importe quelle fonction publique est entrée".

donc, le truc c'est parfois utile et je ne déteste pas les Mutex récursifs dans la mesure où cet article le fait. Je n'ai pas la connaissance historique pour soutenir que la raison de leur inclusion dans Posix est différente de ce que dit l'article, "pour démontrer les attributs de mutex et extensons de fil". Je ne les considère certainement pas par défaut, cependant.

je pense qu'il est prudent de dire que si dans votre conception vous n'êtes pas certain si vous avez besoin d'un verrouillage récursif ou non, alors votre conception est incomplète. Vous regrettera plus tard le fait que vous écrivez le code et vous ne sait pas quelque chose d'aussi fondamental que de savoir si la serrure est déjà verrouillée ou non. Alors ne mettez pas un verrou récursif "juste au cas où".

Si vous savez que vous en avez besoin, utilisez une. Si vous savez que vous n'en avez pas besoin, alors utiliser une serrure non récursive n'est pas seulement une optimisation, c'est aider à imposer une contrainte de la conception. C'est plus utile pour le deuxième verrou à l'échec, que pour elle pour réussir et cacher le fait que vous avez accidentellement fait quelque chose que votre conception dit ne devrait jamais se produire. Mais si vous suivez votre conception, et ne jamais double-verrouiller le mutex, alors vous ne saurez jamais si elle est récursive ou non, et donc un mutex récursive n'est pas directement nuisibles.

Cette analogie peut échouer, mais voici une autre façon de voir les choses. Imaginez que vous ayez le choix entre deux types de pointeur: celui qui abandonne le programme avec une stacktrace lorsque vous déréférencer un pointeur null, et un autre qui renvoie 0 (ou de l'étendre à d'autres types: se comporte comme si le pointeur se réfère à une valeur initialisée objet). Un non-récursive mutex est un peu comme celui qui abandonne, et récursive de mutex est un peu comme celui qui renvoie 0. Ils ont tous les deux potentiellement leurs usages -- les gens vont parfois jusqu'à mettre en œuvre une valeur "silencieuse et non une valeur". Mais dans le cas où votre code est conçu pour ne jamais déréférencer un pointeur null, vous ne voulez pas utilisez par défaut la version qui permet silencieusement que cela se produise.

23
répondu Steve Jessop 2013-01-24 11:19:33

Je ne vais pas directement peser sur le débat mutex versus recursive_mutex, mais j'ai pensé qu'il serait bon de partager un scénario où recursive_mutex est absolument critique à la conception.

en travaillant avec Boost:: asio, Boost:: coroutine (et probablement des choses comme les fibres NT bien que je les connaisse moins), il est absolument essentiel que vos mutex soient récursifs même sans le problème de conception de la rentrée.

la raison en est que la l'approche basée sur coroutine par sa conception même va suspendre l'exécution à l'intérieur une routine et ensuite la reprendre. Cela signifie que deux, de haut niveau, les méthodes d'une classe peuvent "être appelé en même temps sur le même thread", sans aucune sous appels.

6
répondu MB. 2015-02-26 11:47:03