web-dev-qa-db-fra.com

C ++ 11: pourquoi std :: condition_variable utilise std :: unique_lock?

Je suis un peu confus quant au rôle de std::unique_lock Lorsque je travaille avec std::condition_variable. D'après ce que j'ai compris, le documentation , std::unique_lock Est essentiellement un garde-serrure gonflé, avec la possibilité de permuter l'état entre deux verrous.

J'ai jusqu'à présent utilisé pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) à cet effet (je suppose que c'est ce que la STL utilise sur posix). Il faut un mutex, pas un verrou.

Quelle est la différence ici? Le fait que std::condition_variable Traite-t-il std::unique_lock Est-il une optimisation? Si oui, comment est-ce plus rapide?

70
lucas clemente

donc il n'y a pas de raison technique?

J'ai voté en faveur de la réponse de cmeerw parce que je crois qu'il a donné une raison technique. Passons en revue. Imaginons que le comité ait décidé d'attendre condition_variable Sur mutex. Voici le code utilisant cette conception:

void foo()
{
    mut.lock();
    // mut locked by this thread here
    while (not_ready)
        cv.wait(mut);
    // mut locked by this thread here
    mut.unlock();
}

C'est exactement comment on ne devrait pas utiliser un condition_variable. Dans les régions marquées par:

// mut locked by this thread here

il y a un problème de sécurité exceptionnel, et il est grave. Si une exception est levée dans ces zones (ou par cv.wait Lui-même), l'état verrouillé du mutex est divulgué à moins qu'un try/catch soit également placé quelque part pour intercepter l'exception et la déverrouiller. Mais c'est juste plus de code que vous demandez au programmeur d'écrire.

Disons que le programmeur sait comment écrire du code d'exception et sait utiliser unique_lock Pour y parvenir. Maintenant, le code ressemble à ceci:

void foo()
{
    unique_lock<mutex> lk(mut);
    // mut locked by this thread here
    while (not_ready)
        cv.wait(*lk.mutex());
    // mut locked by this thread here
}

C'est beaucoup mieux, mais ce n'est toujours pas une excellente situation. L'interface condition_variable Incite le programmeur à faire tout son possible pour que les choses fonctionnent. Il existe un déréférencement de pointeur nul possible si lk ne fait pas accidentellement référence à un mutex. Et il n'y a aucun moyen pour condition_variable::wait De vérifier que ce thread possède le verrou sur mut.

Oh, rappelez-vous juste, il y a aussi le danger que le programmeur choisisse la mauvaise fonction membre unique_lock Pour exposer le mutex. *lk.release() serait désastreux ici.

Voyons maintenant comment le code est écrit avec l'API condition_variable Qui prend un unique_lock<mutex>:

void foo()
{
    unique_lock<mutex> lk(mut);
    // mut locked by this thread here
    while (not_ready)
        cv.wait(lk);
    // mut locked by this thread here
}
  1. Ce code est aussi simple que possible.
  2. C'est exceptionnellement sûr.
  3. La fonction wait peut vérifier lk.owns_lock() et lever une exception si elle est false.

Ce sont des raisons techniques qui ont motivé la conception de l'API de condition_variable.

De plus, condition_variable::wait Ne prend pas de lock_guard<mutex> Parce que lock_guard<mutex> Est la façon dont vous dites: je possède le verrou sur ce mutex jusqu'à ce que lock_guard<mutex> Soit détruit. Mais lorsque vous appelez condition_variable::wait, Vous libérez implicitement le verrou sur le mutex. Cette action n'est donc pas cohérente avec le cas d'utilisation/l'instruction lock_guard.

Nous avions besoin de unique_lock De toute façon pour que l'on puisse retourner les verrous des fonctions, les placer dans des conteneurs, et verrouiller/déverrouiller les mutex dans des modèles sans portée d'une manière sûre et exceptionnelle, donc unique_lock Était le choix naturel pour condition_variable::wait.

Mise à jour

bamboon a suggéré dans les commentaires ci-dessous que je contraste condition_variable_any, alors voici:

Question: Pourquoi condition_variable::wait N'est-il pas conçu pour que je puisse lui passer n'importe quel type Lockable?

Réponse:

C'est une fonctionnalité vraiment cool à avoir. Par exemple cet article montre du code qui attend un shared_lock (Rwlock) en mode partagé sur une variable de condition (quelque chose d'inouï dans le monde posix, mais néanmoins très utile). Cependant, la fonctionnalité est plus chère.

Le comité a donc introduit un nouveau type avec cette fonctionnalité:

`condition_variable_any`

Avec cet condition_variable adaptateur on peut attendre sur n'importe quel type verrouillable. S'il a des membres lock() et unlock(), vous êtes prêt à partir. Une implémentation correcte de condition_variable_any Nécessite un membre de données condition_variable Et un membre de données shared_ptr<mutex>.

Parce que cette nouvelle fonctionnalité est plus chère que votre condition_variable::wait De base et parce que condition_variable Est un outil de si bas niveau, cette fonctionnalité très utile mais plus chère a été placée dans une classe distincte afin que vous ne payiez que pour cela si vous l'utilisez.

98
Howard Hinnant

Il s'agit essentiellement d'une décision de conception d'API de rendre l'API aussi sûre que possible par défaut (les frais généraux supplémentaires étant considérés comme négligeables). En exigeant de passer un unique_lock au lieu d'un brut mutex les utilisateurs de l'API sont orientés vers l'écriture du code correct (en présence d'exceptions).

Ces dernières années, le langage C++ s'est concentré sur la sécurisation par défaut (mais permet toujours aux utilisateurs de se tirer une balle dans le pied s'ils le souhaitent et d'essayer assez fort).

34
cmeerw