Réveils parasites — pourquoi une variable de condition se réveille « sans notification » et comment attendre correctement sous Windows

· Mis à jour le: · · Windows, Multithreading, Variables de condition, Synchronisation, C++, C#, Win32 API, Investigation de bugs

Historique des révisions (première version, publiée le 22 Aug 2026)
Première publication
Citer cet article(DOI (archive enregistrée): 10.5281/zenodo.22176633)

Les DOI ci-dessous renvoient à des versions déjà archivées et peuvent différer du texte actuel. Pour citer le texte actuel, utilisez l’URL de cette page.

Go Komura (2026). Réveils parasites — pourquoi une variable de condition se réveille « sans notification » et comment attendre correctement sous Windows. KomuraSoft LLC. https://comcomponent.com/fr/blog/windows-condition-variable-spurious-wakeup/

DOI (archive enregistrée)
10.5281/zenodo.22176633
DOI (dernière version enregistrée)
10.5281/zenodo.22176634

« Nous avons mis les données puis notifié, et pourtant le worker lit une file vide et plante. » « Nous avons notifié, et pourtant le thread ne revient parfois pas de l’attente. » Les deux sont des défauts de rendez-vous, mais les endroits à inspecter ne sont pas les mêmes.

Le centre de cet article est un principe : revenir du wait d’une variable de condition ne garantit pas que la condition attendue tient. Une fois compris le réveil parasite, qui revient sans notification, et le réveil volé, où la condition est consommée après la notification, on voit pourquoi while est nécessaire plutôt que if. Le réveil perdu, où une notification est manquée, est traité à part de ces deux-là.

Ce que vous voulez savoir ou résoudre Où lire
Pourquoi une attente se réveille sans notification, et en quoi cela diffère d’un réveil volé Ce qui est garanti au moment du retour, Pourquoi la spécification l’autorise
Vérifier le code correct pour Win32, C++ et C# La forme de base par langage
Un délai d’expiration a été fixé, pourtant le temps d’attente s’allonge Attente calée sur une échéance
Un thread ne revient pas alors qu’il a été notifié Réveil perdu et PulseEvent
Enquêter sur un plantage ou un blocage qui n’apparaît que rarement Démarche d’investigation selon le symptôme

Cet article s’adresse aux développeurs qui écrivent des applications métier et des logiciels de contrôle d’équipement sous Windows. Il confirme le mécanisme à partir des sources primaires et le relie aux implémentations Win32 (C), C++ et C#.

1. La conclusion d’abord

Le code d’attente doit tenir trois règles.

Principe Ce que le code doit respecter
Attendre un état, pas une notification Faire de la condition « la file n’est-elle pas vide ? » et analogues, pas « ai-je été réveillé ? »
Revérifier la condition à chaque retour while (!condition) wait(...) ; en C++, utiliser la surcharge à prédicat wait(lock, pred)
Protéger la vérification et la mise à jour avec le même verrou Mettre à jour l’état avant de notifier, pour qu’aucune notification ne passe dans l’intervalle entre la vérification et l’attente

Win32, C++ et POSIX autorisent, par spécification, des réveils non liés à une notification explicite. De plus, même lorsqu’une notification arrive, un autre thread peut consommer la condition en premier (un réveil volé), donc la revérification est obligatoire. Monitor.Wait de C# s’utilise sous la même discipline, en tenant compte des réveils volés.12345

L’autre mise en garde est de ne pas recréer la notification transitoire d’une variable de condition par une impulsion sur un événement. PulseEvent en particulier peut manquer une notification, et Microsoft indique de ne pas l’utiliser dans les nouvelles applications et d’utiliser une variable de condition à la place.6

La suite de l’article procède dans cet ordre : le fonctionnement des réveils (sections 2 à 4), l’implémentation correcte (section 5), les motifs à éviter et l’investigation (sections 6 et 7), puis une liste de contrôle (section 8).

Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (17 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle

2. Qu’est-ce qu’un réveil parasite — « réveillé » ne veut pas dire « la condition tient »

2.1 Une variable de condition libère le verrou, attend, et le réacquiert avant de revenir

Une variable de condition (condition variable) est un primitive de synchronisation qui fait attendre un thread jusqu’à ce qu’une condition tienne. Sous Win32, on utilise la structure et les API suivantes.1

Rôle Type ou API Win32
Représente la variable de condition CONDITION_VARIABLE
Libère le verrou et attend SleepConditionVariableCS / SleepConditionVariableSRW
Réveille les threads en attente WakeConditionVariable / WakeAllConditionVariable

L’API d’attente libère de façon indivisible la section critique ou le verrou SRW que le thread détient et entre dans l’attente. Après le réveil, elle réacquiert ce verrou avant de revenir à l’appelant.1

Réacquérir le verrou et revenir, toutefois, est autre chose que le fait que la condition attendue par l’application tienne.

2.2 Distinguer une notification authentique, un réveil parasite et un réveil volé

Le traitement des délais d’expiration est renvoyé à la section 5 ; comparons d’abord les trois cas de réveil.

Cas Lien avec une notification explicite Lecture au moment du retour
Réveil authentique Une notification existe La condition tient souvent, mais le seul fait de revenir ne le garantit pas
Réveil parasite Non lié à une notification explicite destinée à réveiller ce thread Revient alors que la condition ne tient toujours pas
Réveil volé (stolen wakeup) Une notification existe Un autre thread a consommé la condition en premier, elle ne tient plus
Trois cas dans lesquels wait revientLe wait d'une variable de condition revient non seulement d'une notification authentique, mais aussi d'un réveil parasite sans notification et d'un réveil volé où une notification est arrivée mais la condition a été consommée en premier, donc la condition doit être revérifiée dans tous les casRevenu de waitNotification authentiqueRéveil parasite (pas de notification)Réveil volé (condition déjà consommée)Revérifier la condition, puis continuer

Figure 1 : Il y a trois chemins de retour depuis wait, et l’appelant ne peut pas les distinguer, donc la condition doit toujours être revérifiée.

Les réveils parasites ne se limitent pas au cas où aucune notification n’a été envoyée nulle part dans le système. Lorsque les notifications arrivent en rafale courte, l’implémentation peut, pour ses propres raisons, réveiller des threads en attente supplémentaires par lots. Du point de vue d’un thread qui n’a pas de notification explicite correspondante, c’est aussi un réveil parasite.

Microsoft Learn indique que les variables de condition sont soumises à la fois aux réveils parasites et aux réveils volés, et demande de revérifier le prédicat, en général dans une boucle while, après le retour de l’attente. Le prédicat désigne ici la condition attendue, par exemple « la file n’est pas vide ».1

2.3 Décider de continuer d’après la condition actuelle, pas d’après la raison du réveil

L’appelant ne peut pas dire lequel des trois l’a ramené. La forme est donc : vérifier la condition elle-même à chaque retour, et attendre à nouveau si elle ne tient pas.

Le while n’empêche pas le réveil parasite ni le réveil volé eux-mêmes. Il est là pour que, quel que soit celui qui se produit, le traitement ne continue pas alors que la condition n’est pas satisfaite. En tenant cette revérification et la protection par le même verrou traitée à la section 5, le code traite correctement chaque chemin de réveil.

3. Pourquoi la spécification l’autorise — une notification précise est coûteuse

3.1 Pour que chaque opération ne paie pas le coût d’une notification stricte

Une implémentation qui ne produit jamais de réveil parasite est possible en théorie. Que POSIX, Windows et C++ l’autorisent néanmoins tient aux performances et à une conception dans laquelle le côté en attente revérifie la condition. La Rationale de pthread_cond_wait de POSIX explique aussi cette décision.3

Implémenter strictement une notification qui « réveille de façon fiable exactement un thread » ajoute un coût de synchronisation supplémentaire à chaque opération de variable de condition, surtout sur multiprocesseurs. L’ordonnanceur s’interpose entre la notification et le réveil, et il y a aussi des interruptions et de la préemption. Autoriser un réveil supplémentaire rare et revérifier du côté en attente maintient l’implémentation plus rapide.3

La boucle de revérification a un autre avantage : elle rend l’intention visible dans le code et rend le code plus robuste. Traiter une notification non comme « une garantie que la condition tient » mais comme un indice que la condition a pu changer permet au code de supporter aussi des changements du côté qui notifie, comme réveiller des threads en trop ou les réveiller par lots.3

3.2 Même sans réveil parasite, le réveil volé demeure

Un réveil volé se produit dans l’intervalle entre la notification et la réacquisition du verrou. Même si le producteur place un élément dans la file et réveille le consommateur A, si le consommateur B prend cet élément avant que A réacquière le verrou, la file est vide au moment où A revient.

Chronologie d'un réveil voléLe producteur place un élément dans la file et réveille le consommateur A en attente, mais avant que A réacquière le verrou le consommateur B acquiert le verrou et prend l'élément, de sorte que la file est vide au moment où A se réveilleConsommateur BProducteurConsommateur A (en attente)Consommateur BProducteurConsommateur A (en attente)Réveillé, attend de réacquérir le verrouLa file est vide (volée)Ajouter un élément à la fileWakeConditionVariableAcquérir le verrou et prendre un élémentRéacquérir le verrou et revenir de waitRevérifier dans le while et attendre à nouveau

Figure 2 : Un réveil volé, dans lequel un troisième thread consomme la condition pendant l’intervalle entre la notification et le réveil, peut se produire dans n’importe quelle implémentation.

Tant qu’un troisième thread peut prendre le verrou en premier, polir l’implémentation ne suffit pas à éliminer ce réveil volé. Même si l’on pouvait supprimer complètement les réveils parasites, la boucle du côté en attente resterait nécessaire tant que les réveils volés existent. Dans ces conditions, la décision de conception est d’autoriser les réveils parasites et de garder les variables de condition rapides.

4. À quelle couche cela apparaît sous Windows

4.1 La discipline de revérification est commune ; on distingue les raisons du réveil

API ou bibliothèque Pourquoi la revérification est nécessaire
Variables de condition Win32 Les réveils parasites et les réveils volés sont documentés explicitement2
WaitOnAddress Autorisé à revenir plus tôt pour des raisons autres qu’un signal à l’adresse indiquée7
C++ std::condition_variable Un wait sans prédicat peut se réveiller de façon parasite48
.NET Monitor.Wait Un autre thread peut consommer la condition entre le réveil et la réacquisition du verrou5

L’exemple officiel Win32 d’une file producteur-consommateur écrit aussi l’attente dans une boucle while.9

WaitOnAddress, disponible à partir de Windows 8, est une API de bas niveau qui attend que la valeur à une adresse indiquée change. Comme exemples de retour anticipé sans signal, la documentation officielle cite un état de mémoire basse, l’abandon d’un wake précédent pour la même adresse, et l’exécution sur une build checked. L’exemple d’utilisation est lui aussi une boucle while qui compare à nouveau la valeur.7

4.2 Le wait à prédicat de C++ exécute la boucle à votre place

La documentation MSVC explique que wait(lock, pred) exécute en pratique ce qui suit.4

while (!Pred())
    wait(Lck);

cppreference indique aussi explicitement qu’un wait sans prédicat peut revenir de façon parasite. Avec la forme à prédicat, on peut laisser cette boucle de revérification à la bibliothèque.8

4.3 En C#, l’attention porte sur le réveil volé et la réacquisition du verrou

Monitor.Wait / Pulse utilisent une file d’attente et une file des prêts. Un thread réveillé par Pulse / PulseAll passe dans la file des prêts et ne quitte Wait qu’après avoir réacquis le verrou. Qu’un autre thread puisse consommer la condition en premier dans cet intervalle est le même point que sous Win32.510

Avec Monitor.Wait de .NET, plutôt que de supposer un réveil sans raison du genre des variables de condition, on le sépare : la même discipline while est nécessaire à cause des réveils volés et des délais d’expiration. La documentation suppose aussi un usage dans lequel le thread réévalue la condition qui l’a fait attendre et rappelle Wait si nécessaire.5

Chaque couche exige de revérifier le prédicatÀ chaque couche, C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE et le WaitOnAddress de bas niveau, la documentation officielle exige de revérifier la condition après le réveilC++ std::condition_variableAu réveil, revérifier la condition (while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

Figure 3 : Quel que soit le langage ou le cadre, chaque couche de primitive d’attente exige officiellement une revérification après le réveil.

5. La façon correcte d’attendre — l’écrire avec while et un prédicat

La forme commune : vérifier la condition, et ne traiter que lorsqu’elle tient

Ce que l’on attend n’est pas « si une notification a été reçue » mais un état partagé protégé par un verrou. Vérifier et mettre à jour le nombre d’éléments de la file ou un drapeau à l’intérieur du même verrou, appeler wait si la condition ne tient pas, et revérifier au retour.

Flux d'une boucle d'attente correcteAcquérir le verrou et vérifier la condition ; si elle ne tient pas, libérer le verrou et dormir ; au réveil, réacquérir le verrou et revenir à la vérification. Ne continuer en détenant le verrou que lorsque la condition tientNonOuiAcquérir le verrouLa condition tient-elle ?wait (libérer le verrou et dormir)Se réveiller (réacquérir le verrou)Traiter en détenant encore le verrou

Figure 4 : Une attente correcte est une boucle, et il n’y a pas d’intervalle entre la vérification de la condition et le traitement (les deux se font pendant que le verrou est détenu).

Avec cette forme, au moment de quitter la boucle, on a confirmé que la condition tient tout en détenant encore le verrou. On consomme l’état tel quel, donc il n’y a pas d’intervalle entre la vérification et le traitement dans lequel un autre thread pourrait s’interposer.

La forme de base en Win32 (C)

CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0;   // état partagé protégé par cs

// Initialiser une seule fois au démarrage (pour une initialisation statique, cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);

// Côté attente (consommateur)
EnterCriticalSection(&cs);
while (queueCount == 0) {                       // toujours while, jamais if
    SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// Ici le verrou est détenu et queueCount > 0 est garanti
--queueCount;
LeaveCriticalSection(&cs);

// Côté notification (producteur)
EnterCriticalSection(&cs);
++queueCount;                                    // mettre à jour l'état à l'intérieur du verrou
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv);                      // notifier après avoir libéré le verrou convient

La distinction à garder est celle entre la mise à jour de l’état et l’endroit où la notification est appelée. ++queueCount doit toujours se faire à l’intérieur du verrou. WakeConditionVariable, en revanche, peut être appelé depuis l’intérieur ou l’extérieur du verrou. Microsoft indique qu’il est en général préférable de réveiller après avoir libéré le verrou, pour réduire les commutations de contexte.1

La forme de base en C++ — prendre le wait à prédicat par défaut

std::mutex m;
std::condition_variable cv;
std::queue<Item> q;

// Côté attente
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); });   // en interne while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();

// Côté notification
{
    std::lock_guard<std::mutex> lk(m);
    q.push(std::move(item));
}
cv.notify_one();

Le nouveau code devrait prendre par défaut le wait à prédicat. Un while (q.empty()) cv.wait(lk); existant est aussi une forme correcte, donc il n’est pas nécessaire de le réécrire seulement parce que la boucle est écrite à la main. Le problème est if (q.empty()) cv.wait(lk);, qui ne revérifie pas.

La forme à prédicat ne prend en charge que la boucle. La discipline de protéger aussi, du côté qui notifie, l’état partagé lu par le prédicat avec le même mutex, et de mettre à jour l’état avant de notifier, reste nécessaire.

La forme de base en C#

private readonly object _gate = new();
private readonly Queue<Item> _queue = new();

// Côté attente
lock (_gate)
{
    while (_queue.Count == 0)          // toujours while, jamais if
    {
        Monitor.Wait(_gate);
    }
    var item = _queue.Dequeue();
}

// Côté notification
lock (_gate)
{
    _queue.Enqueue(item);
    Monitor.Pulse(_gate);              // Pulse de Monitor ne peut être appelé que dans le verrou
}

Monitor.Wait / Pulse / PulseAll s’appellent à l’intérieur d’un bloc synchronisé qui détient le verrou concerné. Hors du verrou, ils lèvent SynchronizationLockException. Il ne faut pas confondre cela avec Win32, où la notification peut être déplacée hors du verrou.10

Attente avec délai d’expiration — calculer le temps restant à partir d’une échéance

Passer à chaque fois la même valeur de délai allonge l’attente à chaque retour d’un réveil parasite. La forme est fixer d’abord l’échéance, et calculer le temps restant à chaque retour.

ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
    ULONGLONG now = GetTickCount64();
    if (now >= deadline) {
        break;                          // délai d'expiration (condition toujours non satisfaite)
    }
    if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
        GetLastError() != ERROR_TIMEOUT) {
        break;                          // en cas d'échec autre qu'un délai, arrêter d'attendre et sortir
    }
    // ERROR_TIMEOUT reçoit sa vérification finale dans la condition du while et le contrôle d'échéance
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
Flux correct d'une attente avec délai d'expirationFixer d'abord l'échéance, et à chaque réveil vérifier la condition et l'échéance ; si l'échéance n'est pas dépassée, recalculer le temps restant et revenir à l'attenteOuiNonOuiNonFixer l'échéanceLa condition tient-elle ?Passer au traitementL'échéance est-elle dépassée ?Traiter le délai d'expirationCalculer le temps restant et attendre

Figure 5 : Une attente avec délai d’expiration ne repasse pas « le même temps d’attente » ; elle recalcule le temps restant à partir de l’échéance.

En C++, on peut laisser ce traitement à la surcharge à prédicat de wait_until, qui prend un instant absolu. Comme elle renvoie la valeur finale du prédicat même en cas de délai, on peut juger d’après le fait que la condition a fini par tenir.4

6. Motifs à éviter

6.1 Vérifier la condition une seule fois avec if

Lorsqu’un réveil parasite ou un réveil volé se produit, le traitement continue alors que la condition n’est pas satisfaite. Cela se manifeste par un plantage ou une corruption de données rares : prise dans une file vide, lecture de données non initialisées, double libération. Passer au while ou au wait à prédicat de la section 5.

6.2 Vérifier ou mettre à jour la condition hors du verrou

Ici il ne s’agit pas de « se réveiller trop souvent » mais d’un réveil perdu (lost wakeup) : manquer la notification et dormir indéfiniment.

Si le côté en attente regarde la condition hors du verrou, décide « pas encore », et que le côté qui notifie met à jour l’état et notifie avant l’entrée dans l’attente, il n’y a personne en attente à cet instant. Le côté en attente entre dans l’attente après la disparition de la notification, et continue d’attendre si aucune autre notification n’arrive.

Chronologie d'un réveil perduSi le côté en attente vérifie la condition hors du verrou et que le côté qui notifie met à jour l'état et notifie dans l'intervalle avant l'entrée en attente, la notification est envoyée à une variable de condition sans attendant et disparaît, et le côté en attente continue d'attendre une notification qui ne viendra plusCôté notificationCôté attenteCôté notificationCôté attentePersonne en attente à cet instantLa notification a déjà disparu, donc pas de réveilVérifier la condition hors du verrou (ne tient pas)Mettre à jour l'état et notifierEntrer dans wait

Figure 6 : Vérifier la condition hors du verrou laisse une notification traverser l’intervalle entre la vérification et wait : un réveil perdu.

La raison pour laquelle l’API d’attente d’une variable de condition rend indivisibles la libération du verrou et l’entrée dans l’attente est de fermer cet intervalle. En tenant la discipline de protéger la vérification et la mise à jour de la condition avec le même verrou et d’entrer dans l’API d’attente en détenant le verrou, on empêche ce réveil perdu.1

6.3 Recréer une notification transitoire par une impulsion sur un événement

Utiliser CreateEvent et SetEvent n’est pas en soi un anti-pattern. Les événements ont des usages légitimes tels que les suivants.

Usage Quand utiliser un événement
Réveiller un seul consommateur Une organisation où le consommateur réveillé traite la file jusqu’à ce qu’elle soit vide
Signaler un arrêt Représenter une instruction d’arrêt qui, une fois levée, n’est plus abaissée, par un événement à réinitialisation manuelle
Attendre avec d’autres cibles d’attente L’inclure dans WaitForMultipleObjects
Rendez-vous entre processus Un usage qu’une variable de condition, qui ne peut pas être partagée entre processus, ne peut pas couvrir

Une variable de condition est un objet en mode utilisateur qui ne peut pas être partagé entre processus. Selon l’usage, le remplacement par une variable de condition n’est même pas possible.1

Ce qui est dangereux, c’est d’essayer de recréer avec un événement le comportement d’une variable de condition « ne réveiller que les threads qui attendent à cet instant et ne laisser aucun état de notification ». Cette idée mène au problème de PulseEvent qui suit.

6.4 Utiliser PulseEvent

PulseEvent est une API qui, sur un événement à réinitialisation manuelle, réveille les threads qui attendent à cet instant et ramène immédiatement l’événement à l’état non signalé. Microsoft indique toutefois explicitement qu’elle n’est pas fiable et existe surtout pour la compatibilité descendante, donc les nouvelles applications ne doivent pas l’utiliser et doivent utiliser une variable de condition à la place.6

La raison est qu’un thread en attente peut être retiré temporairement de l’état d’attente par un APC en mode noyau et revenir à l’attente une fois l’APC terminé. Si PulseEvent est appelé pendant cet intervalle, ce thread ne fait pas partie des « attendants au moment de l’appel » et n’est pas réveillé. Les APC noyau sont un comportement interne de l’OS que l’application ne peut pas contrôler.611

Ce problème est aussi l’avertissement d’analyse statique C28648.12 Un réveil parasite est le problème de « se réveiller trop souvent » ; celui-ci est le problème de « ne pas se réveiller alors qu’il le faudrait ». Comme la notification elle-même est perdue, envelopper l’attente dans un while ne suffit pas à sauver.

6.5 Notifier sans détenir le verrou, avant de mettre à jour l’état

Si l’on notifie d’abord sans détenir le verrou, puis que l’on prend le verrou et que l’on met à jour l’état, le côté réveillé peut voir l’ancien état et se rendormir. Si aucune notification ne suit la mise à jour, il y reste.

On distingue toutefois les deux cas suivants.

Ordre Résultat
Notifier hors du verrou, puis prendre le verrou et mettre à jour l’état Il y a un intervalle où le côté réveillé revérifie avant la mise à jour et se rendort
Notifier, mettre à jour, puis libérer, le tout en détenant le même verrou Le côté en attente ne peut pas vérifier tant qu’il n’a pas réacquis le verrou, donc cet ordre ne cause pas de dégât réel

Plutôt que d’avoir à examiner la sûreté du second à chaque fois, il est plus clair et plus sûr de s’aligner sur l’ordre mettre à jour l’état à l’intérieur du même verrou, puis notifier.

7. Comment enquêter lorsqu’on le rencontre

7.1 Séparer « continue alors que la condition ne tient pas » de « ne se réveille pas »

Symptôme Ce qu’il faut vérifier d’abord Méthode d’investigation
Prise dans une file vide, plantage, résultats manquants Si la condition est revérifiée après l’attente Chercher autour de cv.wait( sans prédicat, SleepConditionVariableCS et Monitor.Wait
Un thread qui devrait se réveiller ne revient pas, le processus se bloque S’il y a un réveil perdu ou un PulseEvent Vérifier la pile de chaque thread dans un dump, et remonter de l’API d’attente bloquée vers le côté qui notifie

Trouver un cv.wait(lk) sans prédicat n’est pas un problème s’il est enveloppé dans un while correct. Collecter les candidats par recherche, puis revoir si l’entourage n’est qu’un if, et si la vérification et la mise à jour de la condition se font sous le même verrou. Ce sont des endroits que l’on peut inspecter sans attendre une reproduction.

Pour un blocage, identifier le lieu d’attente d’après un dump, puis suivre dans le code qui devait notifier, et dans quel ordre. Vérifier les contrôles de condition hors du verrou, les notifications hors du verrou avant la mise à jour de l’état, et PulseEvent.

Flux de triage à partir du symptômeSi le symptôme est que le traitement continue alors que la condition n'est pas satisfaite, chercher dans le code les attentes sans prédicat ; si le symptôme est un thread qui ne se réveille pas, identifier le lieu d'attente d'après un dump et suspecter un réveil perdu ou PulseEventUn défaut qui n'apparaît que rarementLe traitement continue alors que la condition n'est pas satisfaiteUn thread qui devrait se réveiller ne se réveille pasChercher dans le code les attentes sans prédicatIdentifier les threads en attente d'après un dumpRemplacer if par while ou un wait à prédicatSuspecter un réveil perdu et PulseEvent

Figure 7 : Que le symptôme soit « aller trop loin » ou « ne jamais se réveiller » décide à la fois où chercher et avec quels moyens enquêter.

7.2 Comparer avant et après la correction sous les mêmes conditions de stress

Pour reproduire un défaut rare, on élargit la fenêtre de course et on augmente la dispersion des timings. Les méthodes incluent d’utiliser plus de threads que de cœurs physiques, d’insérer un Sleep de diagnostic entre l’attente et la notification, et d’essayer les builds debug et release.

Lorsqu’on compare pour conclure « cela a cessé de se reproduire après la correction de l’attente », appliquer le même stress avant et après la correction.

8. Synthèse — liste de contrôle

Une notification est un indice que la condition a pu changer ; le fondement pour continuer est la condition elle-même, vérifiée à l’intérieur du verrou.

Point de contrôle Forme correcte
Après le retour de l’attente Ne pas chercher à distinguer notification authentique, réveil parasite et réveil volé ; revérifier la condition
Écriture de l’attente L’envelopper dans while. Le nouveau code C++ prend par défaut le wait(lock, pred) à prédicat
État partagé Protéger la vérification et la mise à jour avec le même verrou, et mettre à jour l’état avant de notifier
Endroit d’appel de la notification Win32/C++ peuvent notifier après avoir libéré le verrou. Le Pulse de C# s’appelle à l’intérieur du verrou
Délai d’expiration Calculer le temps restant à partir d’une échéance. En C++, utiliser wait_until à prédicat
Usage des événements Distinguer les usages légitimes des impulsions transitoires, et ne pas dépendre de PulseEvent

Les réveils parasites sont une spécification que Win32, C++ et POSIX ont autorisée volontairement. La réponse est la discipline du côté en attente, pas d’attendre un correctif de l’OS ni de changer de bibliothèque. Monitor.Wait de .NET se distingue des réveils sans raison, mais effectue la même revérification pour faire face aux réveils volés et aux délais d’expiration.

Pour un plantage, commencer par « l’endroit qui continue alors que la condition ne tient pas » ; pour un blocage, par « l’endroit qui manque la notification ». Utiliser aussi cette distinction et la liste de contrôle pour revoir le code existant.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC prend en charge les revues de conception de code multithread, l’investigation des causes (analyse de dump) de plantages et de blocages qui « ne se reproduisent qu’occasionnellement », et la reprise de code de synchronisation héritée (dépendance aux événements, à PulseEvent, etc.) sur une base de variables de condition. Commencer par isoler le symptôme convient ; n’hésitez pas à nous contacter.

Références

  1. Microsoft Learn, Condition Variables. Sur le fait qu’une variable de condition est un objet en mode utilisateur qui libère de façon indivisible le verrou et entre dans l’attente ; sur le fait qu’il y a des réveils parasites (réveils non liés à un réveil explicite) et des réveils volés (un autre thread s’exécute avant le thread réveillé), de sorte que le prédicat devrait être revérifié dans une boucle while après le retour de l’attente ; et sur le fait que la notification est possible depuis l’intérieur ou l’extérieur du verrou, mais que réveiller après avoir libéré le verrou est préférable pour réduire les commutations de contexte. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). Sur le fait de libérer de façon indivisible la section critique indiquée et d’attendre sur la variable de condition ; sur le thread réveillé qui réacquiert la section critique avant de revenir ; sur ERROR_TIMEOUT renvoyé en cas de délai d’expiration ; et sur le fait qu’il y a des réveils parasites et des réveils volés, de sorte que le prédicat devrait être revérifié (en général dans une boucle while) après le retour de l’attente. ↩ ↩2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. Sur le fait que des réveils parasites depuis pthread_cond_wait / pthread_cond_timedwait peuvent se produire ; sur le fait que revenir de wait ne dit rien de la valeur du prédicat, donc le prédicat devrait être réévalué ; et sur le fait que la Rationale indique qu’une implémentation qui « réveille exactement un » peut ralentir les opérations de variable de condition, surtout sur multiprocesseurs, et que l’autorisation des réveils parasites impose une boucle de vérification du prédicat et rend les applications plus robustes. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, condition_variable Class. Sur le fait que wait sans prédicat est indiqué comme se débloquant par notify_one / notify_all et pouvant aussi se réveiller de façon parasite ; sur le fait que wait(lock, pred) à prédicat exécute en pratique while (!Pred()) wait(Lck); ; et sur le fait que wait_for / wait_until ont la même propriété et des surcharges à prédicat. ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn, Monitor.Wait Method. Sur le fait que Wait libère le verrou et entre dans la file d’attente ; sur le fait qu’après un réveil par Pulse / PulseAll on ne revient pas tant que le verrou n’est pas réacquis ; et sur le fait que l’usage prévu est que le thread réveillé réévalue la condition qui l’a fait attendre et rappelle Wait si nécessaire. ↩ ↩2 ↩3 ↩4

  6. Microsoft Learn, PulseEvent function (winbase.h). Sur le fait qu’un thread en attente peut être retiré temporairement de l’état d’attente par un APC en mode noyau et revenir après la fin de l’APC, de sorte que le thread n’est pas libéré si PulseEvent est appelé dans cet intervalle ; et sur le fait que PulseEvent est donc peu fiable, ne doit pas être utilisé dans les nouvelles applications, et doit être remplacé par une variable de condition. ↩ ↩2 ↩3

  7. Microsoft Learn, WaitOnAddress function (synchapi.h). Sur le fait que la fonction qui attend que la valeur à une adresse change est garantie de revenir lorsqu’elle est signalée mais aussi autorisée à revenir pour d’autres raisons ; sur les exemples de réveil anticipé que sont un état de mémoire basse, l’abandon d’un wake précédent pour la même adresse, et l’exécution sur une build checked ; et sur le fait qu’il faut donc comparer à nouveau la valeur après le retour, l’exemple officiel lui-même étant une boucle while. ↩ ↩2

  8. cppreference.com, std::condition_variable::wait. Sur le fait que wait sans prédicat peut se débloquer par un réveil parasite ; et sur le fait que la surcharge à prédicat est équivalente à while (!pred()) wait(lock); et est définie comme une boucle qui réacquiert le verrou et vérifie le prédicat à chaque notification ou réveil parasite. ↩ ↩2

  9. Microsoft Learn, Using Condition Variables. Sur l’exemple officiel qui implémente une file producteur-consommateur avec une section critique et deux variables de condition (BufferNotEmpty et BufferNotFull). L’attente s’effectue à l’intérieur d’une boucle qui vérifie le prédicat. ↩

  10. Microsoft Learn, Monitor.PulseAll Method. Sur le fait que PulseAll déplace les threads de la file d’attente vers la file des prêts, et que le thread suivant de la file des prêts acquiert le verrou lorsque celui-ci est libéré ; et sur le fait que Pulse / PulseAll / Wait ne peuvent être appelés que depuis un bloc synchronisé. ↩ ↩2

  11. Microsoft Learn, Waits and APCs. Sur le fait que les APC noyau s’exécutent de façon préemptive, et que le système interrompt et reprend l’attente en interne sans revenir de l’API d’attente, de sorte qu’un signal transitoire tel que KePulseEvent peut être manqué dans cet intervalle. ↩

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. Sur le fait que l’analyse statique avertit de l’usage de PulseEvent ; sur le fait qu’un thread qui était hors de l’attente à cause d’un APC n’est pas libéré et peut se bloquer indéfiniment ; et sur la consigne de le remplacer par SetEvent ou un autre objet de synchronisation. ↩

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Un réveil parasite est-il un bug de l'OS ou de la bibliothèque ?
Ce n'est pas un bug, c'est un comportement énoncé dans la spécification. SleepConditionVariableCS de Win32, std::condition_variable de C++ et pthread_cond_wait de POSIX ont tous une documentation officielle ou une norme qui indique explicitement qu'un réveil non lié à une notification peut se produire. Une implémentation qui l'interdirait est théoriquement possible, mais elle ralentirait chaque opération de variable de condition (surtout la notification sur multiprocesseurs). Le comportement est donc autorisé en partant du principe que la correction est préservée si le côté en attente revérifie la condition. Le remède n'est donc pas d'attendre un correctif de l'OS, mais d'écrire toujours wait dans une boucle while (ou un wait à prédicat).
Envelopper wait dans un while nuit-il aux performances ?
En pratique le coût est négligeable. La boucle while n'ajoute qu'une vérification de la condition à chaque réveil, et c'est une comparaison légère pendant que le verrou est déjà détenu. Les réveils parasites eux-mêmes sont rares, donc la boucle supplémentaire ne tourne que dans des cas exceptionnels. Le prix de laisser un if, en revanche, est un bug qui ne se reproduit que rarement, dans lequel le traitement continue alors que la condition n'est pas satisfaite ; il n'y a pas de comparaison. Ce qui pèse vraiment sur le coût d'attente d'une variable de condition, c'est la contention du verrou et la fréquence des notifications, pas la présence du while.
Si j'utilise le wait à prédicat de C++, puis-je cesser de penser aux réveils parasites ?
Pour la boucle d'attente, oui : cv.wait(lock, pred) exécute en pratique while (!pred()) wait(lock);, de sorte que les réveils parasites et les réveils volés sont absorbés automatiquement. Le nouveau code C++ devrait prendre par défaut la surcharge à prédicat. Il faut encore protéger avec le même mutex les mises à jour de l'état partagé que lit le prédicat, et le côté qui notifie doit encore mettre à jour l'état avant d'appeler notify. Le wait à prédicat prend en charge la boucle, pas la discipline des verrous.
Le même problème se produit-il avec Monitor.Wait de C# ?
Oui. Un thread qui attend dans Monitor.Wait est réveillé par Pulse/PulseAll, puis réacquiert le verrou avant de quitter Wait, mais dans cet intervalle un autre thread peut avoir acquis le verrou en premier et consommé la condition (un réveil volé). La documentation de Microsoft part aussi du principe que le thread réveillé réévalue la condition qui l'a fait attendre et rappelle Wait si nécessaire. La forme de base en C# est donc aussi while (!condition) Monitor.Wait(gate);. Que Wait/Pulse ne puissent être appelés que depuis une instruction lock est une contrainte qui diffère de Win32.
Y a-t-il aussi des réveils parasites quand on attend un événement avec WaitForSingleObject ?
Dans une attente ordinaire (non alertable), WAIT_OBJECT_0 n'est renvoyé que lorsque l'objet devient effectivement signalé ; il n'y a pas de réveil sans raison du genre que les variables de condition ont. En revanche, « l'événement est devenu signalé » et « la condition de l'application tient » sont deux choses distinctes. Dans une conception où plusieurs consommateurs sont réveillés par le même événement, le thread qui prend le verrou en premier consomme la condition, donc une vérification de la condition après le réveil reste nécessaire. De plus, une conception qui tente de reproduire avec un événement la notification transitoire d'une variable de condition, qui ne réveille que ceux qui attendent à cet instant, aboutit facilement au problème de fiabilité de PulseEvent. Pour attendre qu'un état tienne à l'intérieur d'un processus, une variable de condition est le choix sûr.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog