web-dev-qa-db-fra.com

std :: promise set_value et la sécurité des threads

Je suis un peu confus quant aux exigences en matière de sécurité des threads placées sur std::promise::set_value().

Le standard dit :

Effets: Stocke de manière atomique la valeur r à l'état partagé et rend cet état prêt

Cependant, il est également indiqué que promise::set_value() ne peut être utilisé que pour définir une valeur une fois. S'il est appelé plusieurs fois, un std::future_error est lancé. Vous ne pouvez donc définir la valeur d'une promesse qu'une seule fois.

Et en effet, à peu près tous les tutoriels, exemples de code en ligne ou cas d'utilisation réels de std::promise impliquent un canal de communication entre les threads 2, l'un des threads appelant std::future::get() et l'autre, std::promise::set_value().

Je n'ai jamais vu de cas d'utilisation où plusieurs threads pourraient appeler std::promise::set_value(), et même s'ils le faisaient, tous sauf un provoqueraient une exception std::future_error.

Alors, pourquoi le mandat standard qui appelle à std::promise::set_value() est-il atomique? Quel est le cas d'utilisation pour appeler std::promise::set_value() à partir de plusieurs threads simultanément?


MODIFIER:

Étant donné que la réponse la plus votée ne répond pas vraiment à ma question, je suppose que ce que je demande n’est pas clair. Donc, pour clarifier: je suis conscient de l’avenir et des promesses et de la façon dont elles fonctionnent. Ma question est la suivante: pourquoi la norme insiste-t-elle sur le fait que std::promise::set_value() doit être atomique? C'est une question plus subtile que "pourquoi ne devrait-il pas y avoir de course entre les appels à promise::set_value() et les appels à future::get()"? 

En fait, beaucoup de réponses ici (à tort) répondent que la raison en est que si std::promise::set_value() n'était pas atomique, alors std::future::get() pourrait potentiellement provoquer une condition de concurrence critique. Mais ce n'est pas vrai. 

La seule condition requise pour éviter une situation de concurrence critique est que std::promise::set_value() doit avoir une relation qui se passe avant le avec std::future::get() - en d'autres termes, il doit être garanti que lorsque std::future::wait() revient, std::promise::set_value() est terminé.

Ceci est complètement orthogonal à std::promise::set_value() lui-même étant atomique ou non. Dans une implémentation typique utilisant des variables de condition, std::future::get()/wait() attendait une variable de condition. Ensuite, std::promise::set_value() pourrait de manière non atomique effectuer tout calcul arbitrairement complexe pour définir la valeur réelle. Ensuite, il notifierait la variable de condition partagée (impliquant une barrière de mémoire avec la sémantique de publication), et std::future::get() réveillerait et lisait le résultat en toute sécurité.

Donc, std::promise::set_value() lui-même n'a pas besoin d'être atomique pour éviter une situation de concurrence critique, il doit simplement satisfaire à une relation se passe-avant- avec std::future::get().

Voici à nouveau ma question: pourquoi le standard C++ insiste-t-il sur le fait que std::promise::set_value() doit en réalité être une opération atomique, comme si un appel à std::promise::set_value() était entièrement effectué sous un verrou mutex? Je ne vois aucune raison pour laquelle cette exigence devrait exister, sauf en cas de raison ou de cas d'utilisation de plusieurs threads appelant simultanément std::promise::set_value(). Et je ne peux pas penser à un tel cas d'utilisation, d'où cette question.

17
Siler

S'il ne s'agissait pas d'un magasin atomique, deux threads pourraient simultanément appeler promise::set_value, ce qui a pour effet:

  1. vérifier que le futur n'est pas prêt (c'est-à-dire qu'il a une valeur stockée ou une exception)
  2. stocker la valeur
    • marquer l'état prêt
    • libérer tout ce qui bloque sur l'état partagé en train de devenir prêt

En rendant cette séquence atomique, le premier thread à exécuter (1) obtient tout le chemin jusqu'à (3), et tout autre thread appelant promise::set_value en même temps échouera en (1) et lèvera un future_error avec promise_already_satisfied.

Sans l'atomicité, deux threads pourraient potentiellement stocker leur valeur, puis l'un marquerait avec succès l'état prêt, et l'autre déclencherait une exception, c'est-à-dire le même résultat, sauf qu'il s'agirait de la valeur fil qui a vu une exception qui a traversé.

Dans de nombreux cas, le fil "gagne" n'a pas d'importance, mais si c'est important, sans la garantie d'atomicité, vous devrez envelopper un autre mutex autour de l'appel promise::set_value. D'autres approches, telles que l'échange-comparaison, ne fonctionneraient pas car vous ne pouvez pas vérifier l'avenir (à moins que ce soit un shared_future) pour voir si votre valeur a gagné ou non.

Quand le fil «gagne», vous pouvez donner à chaque fil son propre avenir et utiliser std::experimental::when_any pour collecter le premier résultat disponible.


Editer après quelques recherches historiques:

Bien que ce qui précède (deux threads utilisant le même objet promesse) ne semble pas être un bon cas d'utilisation, cela a certainement été envisagé par l'un des documents contemporains de l'introduction de future à C++: N2744 . Ce document a proposé quelques cas d'utilisation comportant de tels threads conflictuels appelant set_value, et je les citerai ici:

Deuxièmement, considérons les cas d'utilisation où deux opérations asynchrones ou plus sont effectuées en parallèle et "concurrentes" pour satisfaire la promesse. Quelques exemples incluent:

  • Une séquence d’opérations réseau (par exemple, demander une page Web) est exécutée en conjonction avec une attente sur un minuteur.
  • Une valeur peut être extraite de plusieurs serveurs. Pour la redondance, tous les serveurs sont essayés mais seule la première valeur obtenue est nécessaire.

Dans les deux exemples, la première opération asynchrone à terminer est celle qui remplit la promesse. Étant donné que l'une ou l'autre des opérations peut se terminer en second lieu, le code des deux doit être écrit pour que les appels à set_value() puissent échouer.

4
TBBle

Je n'ai jamais vu un cas d'utilisation où plusieurs threads pourraient appeler std :: promise :: set_value (), et même s'ils le faisaient, tous sauf un le ferait provoquer une exception std :: future_error.

Vous avez raté toute l’idée des promesses et des avenirs.

Habituellement, nous avons une paire de promesses et un avenir. la promesse est l'objet que vous Poussez le résultat asynchrone ou l'exception, et le futur est l'objet que vous tirez le résultat asynchrone ou l'exception.

Dans la plupart des cas, l'avenir et la paire de promesses ne résident pas sur le même fil (sinon, nous utiliserions un simple pointeur). ainsi, vous pouvez transmettre la promesse à une fonction asynchrone de bibliothèque, de thread ou de pool de threads, définir le résultat à partir de là et extraire le résultat dans le thread d'appelant.

définir le résultat avec std::promise::set_value doit être atomique, non pas parce que de nombreuses promesses fixent le résultat, mais parce qu'un objet (le futur) qui réside sur un autre thread doit lire le résultat. valeur et en la tirant (en appelant std::future::get ou std::future::then) doit se produire de manière atomique 

Rappelez-vous que chaque futur et promesse a un état partagé , si le résultat d'un thread met à jour l'état partagé et que le résultat est lu à partir de l'état partagé. comme pour tous les états/mémoires partagés en C++, lorsque cela est fait à partir de plusieurs threads, la mise à jour/lecture doit se faire sous un verrou. sinon c'est un comportement indéfini. 

4
David Haim

Ce sont toutes de bonnes réponses, mais il y a un point supplémentaire qui est essentiel. Sans atomicité de la définition d'une valeur, la lecture de la valeur peut être soumise à des effets secondaires observables. 

Par exemple, dans une implémentation naïve:

void thread1()
{
    // do something. Maybe read from disk, or perform computation to populate value
    v = value;
    flag = true;
}

void thread2()
{
    if(flag)
    {
        v2 = v;//Here we have a read problem.
    }
}

Atomicity dans std :: promise <> vous permet d’éviter la condition de concurrence fondamentale entre écrire une valeur dans un thread et en lire dans un autre. Bien sûr, si flag était std :: atomic <> et que les indicateurs de clôture appropriés sont utilisés, vous n’avez plus d’effets secondaires, et std :: promise vous le garantit.

0
xaviersjs