Réveils parasites — pourquoi une variable de condition se réveille « sans notification » et comment attendre correctement sous Windows
· Mis à jour le: · Go Komura · 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 |
flowchart TB
accTitle: Trois cas dans lesquels wait revient
accDescr: Le 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 cas
w["Revenu de wait"] --> a["Notification authentique"]
w --> b["Réveil parasite (pas de notification)"]
w --> c["Réveil volé (condition déjà consommée)"]
a --> r["Revérifier la condition, puis continuer"]
b --> r
c --> r
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.
sequenceDiagram
accTitle: Chronologie d'un réveil volé
accDescr: 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éveille
participant A as Consommateur A (en attente)
participant P as Producteur
participant B as Consommateur B
P->>P: Ajouter un élément à la file
P->>A: WakeConditionVariable
Note over A: Réveillé, attend de réacquérir le verrou
B->>B: Acquérir le verrou et prendre un élément
A->>A: Réacquérir le verrou et revenir de wait
Note over A: La file est vide (volée)
A->>A: Revé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
flowchart TB
accTitle: Chaque couche exige de revérifier le prédicat
accDescr: À 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éveil
cpp["C++ std::condition_variable"] --> rule["Au réveil, revérifier la condition (while)"]
net[".NET Monitor.Wait"] --> rule
win["Win32 CONDITION_VARIABLE"] --> rule
woa["WaitOnAddress"] --> rule
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.
flowchart TB
accTitle: Flux d'une boucle d'attente correcte
accDescr: Acqué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 tient
l["Acquérir le verrou"] --> c{"La condition tient-elle ?"}
c -->|"Non"| s["wait (libérer le verrou et dormir)"]
s --> wk["Se réveiller (réacquérir le verrou)"]
wk --> c
c -->|"Oui"| go["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);
flowchart TB
accTitle: Flux correct d'une attente avec délai d'expiration
accDescr: Fixer 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'attente
d["Fixer l'échéance"] --> c{"La condition tient-elle ?"}
c -->|"Oui"| go["Passer au traitement"]
c -->|"Non"| t{"L'échéance est-elle dépassée ?"}
t -->|"Oui"| to["Traiter le délai d'expiration"]
t -->|"Non"| w["Calculer le temps restant et attendre"]
w --> c
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.
sequenceDiagram
accTitle: Chronologie d'un réveil perdu
accDescr: Si 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 plus
participant W as Côté attente
participant N as Côté notification
W->>W: Vérifier la condition hors du verrou (ne tient pas)
N->>N: Mettre à jour l'état et notifier
Note over N: Personne en attente à cet instant
W->>W: Entrer dans wait
Note over W: La notification a déjà disparu, donc pas de réveil
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.
flowchart TB
accTitle: Flux de triage à partir du symptôme
accDescr: Si 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 PulseEvent
s["Un défaut qui n'apparaît que rarement"] --> a["Le traitement continue alors que la condition n'est pas satisfaite"]
s --> b["Un thread qui devrait se réveiller ne se réveille pas"]
a --> a1["Chercher dans le code les attentes sans prédicat"]
b --> b1["Identifier les threads en attente d'après un dump"]
a1 -.-> a2["Remplacer if par while ou un wait à prédicat"]
b1 -.-> b2["Suspecter 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
- Bonnes pratiques de multithreading en pratique — édition C++
- Bonnes pratiques du multithreading en pratique — édition langage C
- Bonnes pratiques de multithreading en pratique — édition .NET
- Pourquoi préférer l’attente sur événement à Sleep(1) sous Windows
- Pièges de la mémoire partagée et bonnes pratiques concrètes
- Les profondeurs de l’E/S Windows (partie 2) — E/S synchrones et asynchrones : ce que signifie vraiment OVERLAPPED
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.
- Conseil technique et revue de conception
- Analyse de dysfonctionnements et des causes racines
- Développement d’applications Windows
- Contact
Références
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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. ↩
-
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
-
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. ↩
-
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 associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
DllMain et le verrou du chargeur — la vraie raison pour laquelle on vous dit de « ne rien faire dans l'initialisation d'une DLL »
Pourquoi il ne faut pas appeler LoadLibrary ni synchroniser avec d'autres threads depuis DllMain. À partir des sources primaires, cet art...
Pourquoi les arguments se cassent — Les règles des arguments de ligne de commande Windows
Windows passe à CreateProcess une seule chaîne que le destinataire découpe. Traite les règles de CommandLineToArgvW, du CRT et de .NET, A...
Ce qui reste après la mort du parent — garder les processus enfants dans un Job Object
Pourquoi les aides du SDK survivent à une IU tuée et gardent la caméra ou le port COM. Concevoir la durée de vie des processus enfants av...
API du pool de threads Win32 — de la concurrence sans créer de threads, avec CreateThreadpoolWork
Vous multipliez les CreateThread dans le code natif ? Cet article explique, à partir des sources primaires, l'API du pool de threads Win3...
Tubes nommés en pratique — l'IPC standard de Windows, de la conception à la sécurité
Guide pratique des tubes nommés, mécanisme standard de communication inter-processus sous Windows. Cet article organise, à partir des sou...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
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.