Pourquoi ne puis-je pas vérifier si un mutex est verrouillé?

28

C ++ 14 semble avoir omis un mécanisme pour vérifier si an std::mutexest verrouillé ou non. Voir cette question SO:

/programming/21892934/how-to-assert-if-a-stdmutex-is-locked

Il existe plusieurs façons de contourner cela, par exemple en utilisant;

std::mutex::try_lock()
std::unique_lock::owns_lock()

Mais aucune de ces solutions n'est particulièrement satisfaisante.

try_lock()est autorisé à retourner un faux négatif et a un comportement indéfini si le thread actuel a verrouillé le mutex. Il a également des effets secondaires. owns_lock()nécessite la construction d'un unique_locksur le dessus de l'original std::mutex.

Évidemment, je pourrais rouler le mien, mais je préfère comprendre les motivations de l'interface actuelle.

La capacité de vérifier le statut d'un mutex (par exemple std::mutex::is_locked()) ne me semble pas être une demande ésotérique, donc je soupçonne que le Comité de normalisation a délibérément omis cette caractéristique plutôt que d'être une erreur.

Pourquoi?

Edit: Ok donc peut-être que ce cas d'utilisation n'est pas aussi courant que je m'y attendais, donc je vais illustrer mon scénario particulier. J'ai un algorithme d'apprentissage automatique qui est distribué sur plusieurs threads. Chaque thread fonctionne de manière asynchrone et retourne dans un pool maître une fois qu'il a terminé un problème d'optimisation.

Il verrouille ensuite un mutex maître. Le thread doit ensuite choisir un nouveau parent à partir duquel muter une progéniture, mais ne peut choisir que des parents qui n'ont actuellement pas de progéniture qui sont optimisés par d'autres threads. J'ai donc besoin d'effectuer une recherche pour trouver des parents qui ne sont pas actuellement verrouillés par un autre thread. Il n'y a aucun risque que l'état du mutex change pendant la recherche, car le mutex du thread maître est verrouillé. Évidemment, il existe d'autres solutions (j'utilise actuellement un indicateur booléen), mais je pensais que le mutex offre une solution logique à ce problème, car il existe à des fins de synchronisation inter-thread.

quant
la source
42
Vous ne pouvez pas vraiment vérifier si un mutex est verrouillé, car une nanoseconde après la vérification, il peut être déverrouillé ou verrouillé. Donc, si vous avez écrit "if (mutex_is_locked ()) ..." alors mutex_is_locked pourrait retourner le résultat correct, mais au moment où le "si" est exécuté, il est faux.
gnasher729
1
Ce ^. De quelles informations utiles espérez-vous obtenir is_locked?
inutile
3
Cela ressemble à un problème XY. Pourquoi essayez-vous d'empêcher la réutilisation des parents uniquement pendant la génération d'un enfant? Avez-vous l'obligation qu'un parent ne puisse avoir qu'une seule progéniture? Votre serrure n'empêchera pas cela. N'avez-vous pas des générations claires? Sinon, savez-vous que les individus qui peuvent être optimisés plus rapidement ont une meilleure condition physique, car ils peuvent être sélectionnés plus souvent / plus tôt? Si vous utilisez des générations, pourquoi ne sélectionnez-vous pas tous les parents à l'avance, puis laissez-les récupérer les parents d'une file d'attente? La génération de descendants est-elle vraiment si chère que vous avez besoin de plusieurs threads?
amon
10
@quant - Je ne vois pas pourquoi vos mutex d'objet parent dans votre exemple d'application doivent être des mutex du tout: si vous avez un mutex maître qui est verrouillé chaque fois qu'ils sont définis, vous pouvez simplement utiliser une variable booléenne pour indiquer leur statut.
Periata Breatta
4
Je ne suis pas d'accord avec la dernière phrase de la question. Une simple valeur booléenne est bien plus propre qu'un mutex ici. Faites-en un bool atomique si vous ne voulez pas verrouiller le mutex maître pour "retourner" un parent.
Sebastian Redl

Réponses:

53

Je peux voir au moins deux problèmes graves avec l'opération suggérée.

Le premier a déjà été mentionné dans un commentaire de @ gnasher729 :

Vous ne pouvez pas vraiment vérifier si un mutex est verrouillé, car une nanoseconde après la vérification, il peut être déverrouillé ou verrouillé. Donc , si vous avez écrit if (mutex_is_locked ()) …alors mutex_is_lockedpu retourner le résultat correct, mais au moment où l' ifest exécuté, il est faux.

La seule façon de s'assurer que la propriété «est actuellement verrouillé» d'un mutex ne change pas est de bien le verrouiller vous-même.

Le deuxième problème que je vois est que, sauf si vous verrouillez un mutex, votre thread ne se synchronise pas avec le thread qui avait précédemment verrouillé le mutex. Par conséquent, il n'est même pas bien défini de parler «avant» et «après» et si le mutex est verrouillé ou non revient à se demander si le chat de Schrödiger est actuellement en vie sans tenter d'ouvrir la boîte.

Si je comprends bien, les deux problèmes seraient sans objet dans votre cas particulier grâce au verrouillage du mutex maître. Mais cela ne me semble pas être un cas particulièrement courant, je pense donc que le comité a fait ce qu'il fallait en n'ajoutant pas de fonction qui pourrait être quelque peu utile dans des scénarios très spéciaux et causer des dommages dans tous les autres. (Dans l'esprit de: "Rendre les interfaces faciles à utiliser correctement et difficiles à utiliser incorrectement.")

Et si je peux dire, je pense que la configuration que vous avez actuellement n'est pas la plus élégante et pourrait être refactorisée pour éviter tout problème. Par exemple, au lieu que le thread principal vérifie tous les parents potentiels pour celui qui n'est pas actuellement verrouillé, pourquoi ne pas conserver une file d'attente de parents prêts? Si un thread veut en optimiser un autre, il saute le suivant de la file d'attente et dès qu'il a de nouveaux parents, il les ajoute à la file d'attente. De cette façon, vous n'avez même pas besoin du thread principal en tant que coordinateur.

5gon12eder
la source
Merci, c'est une bonne réponse. La raison pour laquelle je ne veux pas maintenir une file d'attente de parents prêts est que je dois préserver l'ordre dans lequel les parents ont été créés (car cela dicte leur durée de vie). Cela se fait facilement avec une file d'attente LIFO. Si je commence à arracher des choses, je devrais maintenir un mécanisme de commande distinct qui compliquerait les choses, d'où l'approche actuelle.
quant
14
@quant: Si vous avez deux objectifs pour mettre en file d'attente les parents, vous pouvez le faire avec deux files d'attente ....
@quant: vous supprimez un élément (au maximum) une fois, mais effectuez probablement un traitement à chaque fois plusieurs fois, vous optimisez donc le cas rare au détriment du cas commun. C'est rarement souhaitable.
Jerry Coffin
2
Mais il est raisonnable de se demander si le thread actuel a verrouillé le mutex.
Expiation limitée
@LimitedAtonement Pas vraiment. Pour ce faire, le mutex doit stocker des informations supplémentaires (identifiant de thread), ce qui le rend plus lent. Les mutex récursifs le font déjà, vous devriez plutôt les utiliser.
StaceyGirl
9

Il semble que vous utilisiez les mutex secondaires non pas pour verrouiller l'accès à un problème d'optimisation, mais pour déterminer si un problème d'optimisation est optimisé en ce moment ou non.

C'est totalement inutile. J'aurais une liste de problèmes qui doivent être optimisés, une liste de problèmes en cours d'optimisation en ce moment et une liste de problèmes qui ont été optimisés. (Ne prenez pas littéralement «liste», pensez à «toute structure de données appropriée).

Les opérations d'ajout d'un nouveau problème à la liste des problèmes non optimisés, ou de déplacement d'un problème d'une liste à l'autre, seraient effectuées sous la protection du mutex "maître" unique.

gnasher729
la source
1
Vous ne pensez pas qu'un objet de type std::mutexest approprié pour une telle structure de données?
quant
2
@quant - non. std::mutexrepose sur une implémentation de mutex définie par le système d'exploitation qui peut très bien prendre des ressources (par exemple des poignées) qui sont limitées et lentes à allouer et / ou à exploiter. L'utilisation d'un seul mutex pour verrouiller l'accès à une structure de données interne est susceptible d'être beaucoup plus efficace et peut-être aussi plus évolutive.
Periata Breatta du
1
Tenez également compte des variables de condition. Ils peuvent simplifier de nombreuses structures de données comme celle-ci.
Cort Ammon - Rétablir Monica
2

Comme d'autres l'ont dit, il n'y a pas de cas d'utilisation où is_lockedsur un mutex est d'un quelconque avantage, c'est pourquoi la fonction n'existe pas.

Le cas avec lequel vous rencontrez un problème est incroyablement courant, c'est essentiellement ce que font les threads de travail, qui sont l'une, sinon la mise en œuvre la plus courante des threads.

Vous avez une étagère avec 10 boîtes dessus. Vous avez 4 travailleurs travaillant avec ces boîtes. Comment vous assurez-vous que les 4 travailleurs travaillent sur des boîtes différentes? Le premier travailleur prend une boîte sur l'étagère avant de commencer à y travailler. Le deuxième travailleur voit 9 boîtes sur l'étagère.

Il n'y a pas de mutex pour verrouiller les boîtes, donc voir l'état du mutex imaginaire sur la boîte n'est pas nécessaire, et abuser d'un mutex en tant que booléen est tout simplement faux. Le mutex verrouille l'étagère.

Peter - Unban Robert Harvey
la source
1

En plus des deux raisons données dans la réponse de 5gon12eder ci-dessus, je voudrais ajouter que ce n'est ni nécessaire ni souhaitable.

Si vous détenez déjà un mutex, vous feriez mieux de savoir que vous le tenez! Vous n'avez pas besoin de demander. Tout comme avec un bloc de mémoire ou toute autre ressource, vous devez savoir exactement si vous le possédez ou non, et quand il convient de libérer / supprimer la ressource.
Si ce n'est pas le cas, votre programme est mal conçu et vous vous dirigez vers des ennuis.

Si vous devez accéder à la ressource partagée protégée par le mutex et que vous ne possédez pas déjà le mutex, vous devez acquérir le mutex. Il n'y a pas d'autre option, sinon la logique de votre programme n'est pas correcte.
Vous pouvez trouver le blocage acceptable ou inacceptable, dans les deux cas lock()ou try_lock()donner le comportement que vous souhaitez. Tout ce que vous devez savoir, positivement et sans aucun doute, c'est si vous avez réussi à acquérir le mutex (la valeur de retour de try_lockvous l'indique). Peu importe que quelqu'un d'autre le détienne ou que vous ayez un faux échec.

Dans tous les autres cas, carrément, cela ne vous regarde pas. Vous n'avez pas besoin de savoir, et vous ne devriez pas savoir, ni faire d'hypothèses (pour les problèmes d'actualité et de synchronisation mentionnés dans l'autre question).

Damon
la source
1
Que faire si je souhaite effectuer une opération de classement sur les ressources actuellement disponibles pour le verrouillage?
quant
Mais est-ce quelque chose qui est réaliste de se produire? Je trouverais cela plutôt inhabituel. Je dirais que les ressources ont déjà un classement intrinsèque, alors vous devrez d'abord faire (acquérir le verrou) le plus important. Exemple: Vous devez mettre à jour la simulation physique avant le rendu. Ou, le classement est plus ou moins délibéré, alors vous pouvez aussi try_lockla première ressource, et si celle-ci échoue, essayez la seconde. Exemple: Trois connexions persistantes et regroupées au serveur de base de données, et vous devez en utiliser une pour envoyer une commande.
Damon
4
@quant - "une opération de classement sur les ressources actuellement disponibles pour le verrouillage" - en général, faire ce genre de chose est un moyen très simple et rapide d'écrire du code qui se bloque d'une manière que vous avez du mal à comprendre. Rendre déterministes l' acquisition et la libération de verrous est, dans presque tous les cas, la meilleure politique. La recherche d'une serrure basée sur un critère susceptible de changer est source de problèmes.
Periata Breatta
@PeriataBreatta Mon programme est intentionnellement indéterminé. Je vois maintenant que cet attribut n'est pas commun, donc je peux comprendre l'omission de fonctionnalités comme is_locked()cela pourrait faciliter un tel comportement.
quant
Le classement et le verrouillage @quant sont des problèmes entièrement distincts. Si vous souhaitez en quelque sorte trier ou réorganiser une file d'attente avec un verrou, verrouillez-la, triez-la, puis déverrouillez-la. Si vous en avez besoin is_locked, il existe une bien meilleure solution à votre problème que celle que vous avez en tête.
Peter - Unban Robert Harvey
1

Vous souhaiterez peut-être utiliser atomic_flag avec l'ordre de mémoire par défaut. Il n'a pas de races de données et ne lève jamais d'exceptions comme le fait mutex avec plusieurs appels de déverrouillage (et abandonne de manière incontrôlable, je pourrais ajouter ...). Alternativement, il y a atomic (par exemple atomic [bool] ou atomic [int] (avec des crochets triangulaires, pas [])), qui a de belles fonctions comme load et compare_exchange_strong.

Andrew
la source
1

Je veux ajouter un cas d'utilisation pour cela: cela permettrait à une fonction interne de garantir comme condition préalable / affirmation que l'appelant détient effectivement le verrou.

Pour les classes avec plusieurs de ces fonctions internes, et éventuellement de nombreuses fonctions publiques les appelant, cela pourrait garantir que quelqu'un ajoutant une autre fonction publique appelant la fonction interne a effectivement acquis le verrou.

class SynchronizedClass
{

public:

void publicFunc()
{
  std::lock_guard<std::mutex>(_mutex);

  internalFuncA();
}

// A lot of code

void newPublicFunc()
{
  internalFuncA(); // whops, forgot to acquire the lock
}


private:

void internalFuncA()
{
  assert(_mutex.is_locked_by_this_thread());

  doStuffWithLockedResource();
}

};
B3ret
la source