web-dev-qa-db-fra.com

Est-il correct de lire un drapeau booléen partagé sans le verrouiller lorsqu'un autre thread peut le définir (au plus une fois)?

Je voudrais que mon fil se ferme plus gracieusement, alors j'essaie de mettre en œuvre un mécanisme de signalisation simple. Je ne pense pas que je veux un thread entièrement événementiel, j'ai donc un travailleur avec une méthode pour l'arrêter graceully en utilisant une section critique Monitor (équivalent à un C # lock je crois):

DrawingThread.h

class DrawingThread {
    bool stopRequested;
    Runtime::Monitor CSMonitor;
    CPInfo *pPInfo;
    //More..
}

DrawingThread.cpp

void DrawingThread::Run() {
    if (!stopRequested)
        //Time consuming call#1
    if (!stopRequested) {
        CSMonitor.Enter();
        pPInfo = new CPInfo(/**/);
        //Not time consuming but pPInfo must either be null or constructed. 
        CSMonitor.Exit();
    }
    if (!stopRequested) {
        pPInfo->foobar(/**/);//Time consuming and can be signalled
    }
    if (!stopRequested) {
        //One more optional but time consuming call.
    }
}


void DrawingThread::RequestStop() {
    CSMonitor.Enter();
    stopRequested = true;
    if (pPInfo) pPInfo->RequestStop();
    CSMonitor.Exit();
}

Je comprends (au moins sous Windows) Monitor/locks sont la primitive de synchronisation des threads la moins chère, mais je tiens à éviter une utilisation excessive. Dois-je envelopper chaque lecture de ce drapeau booléen? Il est initialisé à false et défini une seule fois sur true lorsque l'arrêt est demandé (s'il est demandé avant la fin de la tâche).

Mes tuteurs ont conseillé de protéger même les bool car la lecture/écriture peut ne pas être atomique. Je pense que ce drapeau à un coup est l'exception qui confirme la règle?

46
John

Il n'est jamais OK de lire quelque chose éventuellement modifié dans un thread différent sans synchronisation. Le niveau de synchronisation requis dépend de ce que vous lisez réellement. Pour les types primitifs, vous devriez jeter un œil aux lectures atomiques, par ex. sous la forme de std::atomic<bool>.

La raison pour laquelle la synchronisation est toujours nécessaire est que les processeurs auront éventuellement les données partagées dans une ligne de cache. Il n'a aucune raison de mettre à jour cette valeur en une valeur éventuellement modifiée dans un thread différent s'il n'y a pas de synchronisation. Pire encore, s'il n'y a pas de synchronisation, il peut écrire la mauvaise valeur si quelque chose stocké près de la valeur est modifié et synchronisé.

44
Dietmar Kühl

L'affectation booléenne est atomique. Ce n'est pas ça le problème.

Le problème est qu'un thread peut ne pas voir les modifications apportées à une variable par un thread différent en raison de la réorganisation des instructions du compilateur ou du processeur ou de la mise en cache des données (c'est-à-dire que le thread qui lit l'indicateur booléen peut lire une valeur mise en cache, au lieu de la mise à jour réelle valeur).

La solution est une barrière de mémoire, qui est en effet ajoutée implicitement par les instructions de verrouillage, mais pour une seule variable, c'est exagéré. Déclarez-le simplement comme std::atomic<bool>.

12
Tudor

La réponse, je crois, est "cela dépend". Si vous utilisez C++ 03, le threading n'est pas défini dans la norme, et vous devrez lire ce que disent votre compilateur et votre bibliothèque de threads, bien que ce genre de chose soit généralement appelé une "course bénigne" - et est généralement OK .

Si vous utilisez C++ 11, les races bénignes sont un comportement indéfini. Même lorsqu'un comportement non défini n'a pas de sens pour le type de données sous-jacent. Le problème est que les compilateurs peuvent supposer que les programmes n'ont pas de comportement indéfini, et faire des optimisations en fonction de cela (voir aussi les parties 1 et 2 liées à partir de là). Par exemple, votre compilateur pourrait décider de lire l'indicateur une fois et de mettre en cache la valeur car c'est un comportement non défini d'écrire dans la variable dans un autre thread sans une sorte de mutex ou de barrière de mémoire.

Bien sûr, il se pourrait bien que votre compilateur promette de ne pas faire cette optimisation. Vous devrez regarder.

La solution la plus simple consiste à utiliser std::atomic<bool> en C++ 11, ou quelque chose comme atomic_ops de Hans Boehm ailleurs.

6
Max Lybbert

Non, vous devez protéger chaque accès, car les compilateurs modernes et les processeurs réordonnent le code sans avoir à l'esprit vos tâches de multithreading. L'accès en lecture à partir de différents threads peut fonctionner, mais ne doit pas fonctionner.

1
Jörg Beyer