Pourquoi préférer l'attente sur événement à Sleep(1) sous Windows
· Mis à jour le: · Go Komura · Développement Windows, Synchronisation, Événements, Timer, Conception
Dans notre article précédent, Guide pratique du soft real-time sous Windows, nous avions abordé le fait d’éviter les boucles périodiques qui s’appuient sur Sleep.
Cette fois, nous nous concentrons sur un point précis de cette discussion : pourquoi il vaut mieux préférer une attente sur événement à une attente courte sur timer.
Sous Windows, une conception qui « vérifie à intervalle fixe » à l’aide de Sleep(1) ou d’attentes avec des délais courts est inévitablement affectée par la granularité de l’horloge système et par le délai d’ordonnancement qui s’ensuit.
Avec les réglages courants, une résolution de timer de la plateforme de l’ordre de 15,6 ms est la référence habituelle, si bien que même en pensant « je revérifie dans 1 ms », l’attente réellement obtenue est souvent bien plus grossière.
À l’inverse, si ce que l’on veut vraiment attendre est un « événement » plutôt que le « temps » — l’arrivée d’un travail, la fin d’une E/S, une demande d’arrêt, un changement d’état — il n’est pas nécessaire d’aller vérifier à intervalle fixe. Le côté où l’événement se produit signale, et le côté qui attend attend sur l’événement. Cette approche est plus naturelle, aussi bien pour la latence que pour le CPU et la consommation.
Les questions auxquelles cet article souhaite répondre sont ces quatre-là :
- Pourquoi
Sleep(1)et les attentes courtes sur timer sont-elles moins précises qu’on ne le pense ? - Pourquoi les attentes sur événement sont-elles moins soumises à cette limite ?
- Dans quelles situations faut-il choisir un événement plutôt qu’un timer ?
- Dans quels cas faut-il quand même utiliser un timer ?
1. La conclusion d’abord
- Si vous attendez l’arrivée d’un travail ou la fin d’une E/S, attendez sur un événement, pas sur un timer.
- Les attentes chronométrées sous Windows sont inévitablement affectées par la granularité de l’horloge système.
Sleep(1)ne signifie pas « se réveiller exactement 1 ms plus tard ».- Et même une fois le délai écoulé, le thread devient d’abord simplement prêt — son exécution immédiate n’est pas garantie.
- C’est pourquoi une conception qui « attend en réalité un événement mais va vérifier avec un timer » perd à la fois en latence et en consommation.
- Il est plus propre de réserver les timers aux cas où le temps lui-même est réellement la condition.
En termes pratiques, cela revient à peu près à ceci :
- « Envoyer des métriques toutes les 5 secondes » -> le travail d’un timer
- « Se déclencher dès qu’un travail arrive dans la file » -> le travail d’un événement / sémaphore / variable de condition /
WaitOnAddress - « Poursuivre une fois l’E/S terminée » -> le travail d’une complétion / d’un événement
- « S’arrêter quand une demande d’arrêt arrive » -> le travail d’un stop event / d’une annulation
2. Où est le problème
2.1 Les attentes chronométrées sont liées à la granularité de l’horloge système
La précision du délai des wait functions de Windows dépend de la résolution de l’horloge système.
C’est la même chose pour Sleep : les millisecondes indiquées ne sont pas garanties d’être respectées « exactement telles quelles ».
Le point important ici est que spécifier 1 ms ne veut pas dire se réveiller 1 ms plus tard.
2.2 Même l’échéance atteinte, l’exécution n’est pas forcément immédiate
Ce qui complique encore les choses, c’est que le thread ne commence pas à s’exécuter au moment même où le délai s’écoule.
Comme le rappelle la documentation de Sleep, une fois l’intervalle d’attente terminé, le thread devient prêt (ready), mais rien ne garantit qu’il obtienne le CPU et s’exécute immédiatement.
Il est influencé par les autres threads, les priorités, les états d’inactivité du CPU, les DPC / ISR, la contention de verrous, etc.
Autrement dit, une attente courte sur timer comporte au moins deux niveaux d’incertitude :
- La détermination même du délai est tirée par la granularité du timer
- Même après le délai, le moment où l’exécution démarre dépend de l’ordonnanceur
2.3 Sleep(1) ne signifie pas une période de 1 ms
En voyant Sleep(1), on est tenté de le lire comme « une boucle qui tourne toutes les 1 ms ».
Mais en réalité, il ne faut pas le lire ainsi.
while (!g_stop)
{
Step();
Sleep(1);
}
Ce que fait réellement cette boucle est ceci :
- Le temps d’exécution de
Step()s’ajoute à chaque itération - Le temps d’attente de
Sleep(1)lui-même est tiré par la granularité - Même après le réveil, rien ne garantit une exécution immédiate
3. Pourquoi l’attente sur événement a l’avantage
3.1 La fin de l’attente devient un « signal », pas une « expiration de délai »
L’attente sur événement a l’avantage de changer le sens même de l’attente.
Une attente sur timer fonctionne ainsi :
- Même si rien ne s’est encore produit
- On se réveille quand un temps fixe s’est écoulé
- Une fois réveillé, on vérifie si quelque chose s’est produit
Une attente sur événement fonctionne ainsi :
- Le côté où quelque chose s’est produit signale
- Une fois signalée, l’attente est satisfaite
- Au moment du réveil, il y a déjà une raison
flowchart LR
start["thread en attente"] --> q{"qu'attendez-vous réellement ?"}
q -- "qu'un moment arrive" --> timer["timer / waitable timer"]
q -- "l'arrivée d'un travail" --> event["event / semaphore / condition variable"]
q -- "un changement de valeur" --> addr["WaitOnAddress"]
q -- "la fin d'une E/S" --> io["completion / event"]
q -- "une demande d'arrêt" --> stop["stop event / cancellation"]
3.2 Choisir l’outil selon ce que l’on attend
En première approche, ce tableau couvre la plupart des décisions.
| Ce que vous voulez attendre | Mauvais exemple | Premier choix |
|---|---|---|
| L’arrivée d’un travail dans une file | TryPop avec Sleep(1) |
event / semaphore |
| La fin d’une E/S | Surveiller l’état avec un timer | event d’E/S recouvrante / IOCP |
| L’arrivée d’une demande d’arrêt | Vérifier un stop flag toutes les 100 ms | stop event / cancellation |
| Un changement de valeur au sein du même processus | while (flag == 0) Sleep(1) |
WaitOnAddress |
| L’arrivée d’un moment précis | Forcer cela sur un événement | timer / waitable timer |
3.3 L’événement n’est pas non plus magique
L’attente sur événement a l’avantage de ne pas avoir besoin de se réveiller à la granularité du timer, mais cela ne signifie pas que le thread s’exécute avec un délai strictement nul dès l’instant où il est signalé.
Même une attente sur événement subit ces influences :
- la latence de l’ordonnanceur
- la priorité du thread
- l’état d’alimentation du CPU
- la contention de verrous
- les défauts de page
- les DPC / ISR
Cela dit, on élimine au moins le type d’attente superflu où le thread « dort jusqu’au prochain tick du timer ».
4. Anti-patterns typiques
4.1 Scruter une file avec Sleep(1)
C’est ce que l’on voit le plus souvent.
for (;;)
{
if (g_stop)
{
break;
}
WorkItem item;
if (TryPop(item))
{
Process(item);
continue;
}
Sleep(1);
}
Cette écriture paraît simple au premier abord, mais elle pose trois problèmes :
- Le thread se réveille périodiquement même quand la file est vide
- La latence est tirée par la granularité du timer
- Elle est également désavantageuse en consommation
4.2 Surveiller un état avec Thread.Sleep(1) / Task.Delay(1)
La même odeur se retrouve aussi en C# / .NET.
while (!stoppingToken.IsCancellationRequested)
{
if (_queue.TryDequeue(out WorkItem? item))
{
await ProcessAsync(item, stoppingToken);
continue;
}
await Task.Delay(1, stoppingToken);
}
L’apparence est douce et async, mais l’essence de la conception reste du polling.
5. Comment corriger cela
5.1 Le producteur signale à l’arrivée
Si vous attendez l’arrivée dans une file, changez la conception pour que le producteur signale, au lieu de faire du polling.
- Le producteur dépose un item dans la file
- Juste après l’avoir déposé, il appelle
SetEvent - Le consommateur attend avec
WaitForSingleObjectouWaitForMultipleObjects - À son réveil, il vide la file
5.2 Attendre work et stop en même temps avec WaitForMultipleObjects
Pour un worker simple, cette forme est facile à suivre.
HANDLE waits[2] = { _stopEvent, _workEvent };
for (;;)
{
DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (rc == WAIT_OBJECT_0)
{
return;
}
if (rc != WAIT_OBJECT_0 + 1)
{
throw std::runtime_error("WaitForMultipleObjects failed.");
}
DrainQueue();
}
Les points clés de cet exemple sont au nombre de trois :
Sleep(1)a disparu- Le producteur appelle
SetEventquand un item arrive - Le worker attend simultanément sur
stopet surwork
5.3 Au sein d’un même processus, WaitOnAddress est aussi une option
Si, au sein d’un même processus, tout ce que l’on veut est « attendre qu’une valeur change », WaitOnAddress est également une option solide.
Comme repère pour choisir, cela donne à peu près ceci :
- Entre processus ou pour des cibles d’attente générales -> event / semaphore / waitable object
- Changement de valeur léger au sein du même processus ->
WaitOnAddress
6. Les cas où l’on utilise quand même un timer
6.1 Quand le temps lui-même est la condition
Bien sûr, il existe de vrais cas d’usage pour les timers.
- Envoyer des métriques toutes les 5 secondes
- Réessayer après 200 ms
- Nettoyer un cache toutes les minutes
- Attendre jusqu’à une échéance et traiter cela comme un timeout
Dans ces cas, ce que l’on veut attendre est vraiment le temps.
6.2 Utiliser un waitable timer
Si vous attendez « le temps lui-même » sous Windows, utiliser un waitable timer rend l’intention plus claire que d’empiler des appels à Sleep de manière désordonnée.
6.3 Ne pas faire de timeBeginPeriod une habitude
Quand la précision des attentes courtes sur timer commence à poser problème, on est tenté d’ajouter timeBeginPeriod(1).
Mais ce ne devrait pas être le premier choix par défaut.
Il y a trois raisons à cela :
- Cela a un coût en énergie / performance
- Sur les versions récentes de Windows, le comportement est un peu plus complexe
- Cela signifie souvent que la cause racine n’a pas été corrigée
7. Liste de vérification pour la revue
- Construisez-vous une boucle de vérification avec
Sleep(1)/Thread.Sleep(1)/Task.Delay(1)? - Faites-vous du polling par timer alors que ce que vous attendez réellement est l’arrivée dans une file, la fin d’une E/S ou une demande d’arrêt ?
- La conception permet-elle au producteur / au côté complétion de signaler ?
- Peut-on attendre
stopetworkensemble en une seule attente ? - Pour un changement de valeur au sein du même processus, peut-on l’écrire avec
WaitOnAddress? - Là où un timer est utilisé, ce que vous voulez vraiment attendre est-il réellement « le temps » ?
8. Résumé
Sous Windows, une conception qui utilise des attentes courtes sur timer pour « vérifier à intervalle fixe » est inévitablement affectée par la granularité du timer et par l’ordonnanceur.
De ce fait, Sleep(1) et les délais courts ne constituent pas une attente aussi précise qu’il n’y paraît.
À l’inverse, si ce que l’on veut vraiment attendre est un « événement » — l’arrivée d’un travail, la fin d’une E/S, une demande d’arrêt, un changement d’état — l’attente sur événement est le choix le plus naturel.
En résumé, tout tient en cette seule ligne :
Attendez sur un timer pour le temps, attendez sur un événement pour les événements.
Le simple fait de clarifier cette frontière apporte les bénéfices suivants :
- la latence devient plus facile à raisonner
- les réveils périodiques inutiles diminuent
- la consommation d’énergie s’améliore
- l’intention du code devient plus lisible
9. Références
- Sleep function (Win32)
- Wait Functions
- WaitForSingleObject function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- SetWaitableTimer function
- Thread.Sleep Method (.NET)
- Results for the Idle Energy Efficiency Assessment
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Introduction à l'ADR (Architecture Decision Record) — la méthode minimale pour conserver « pourquoi on a choisi cette conception » sur un petit projet
Le code ne dit jamais pourquoi il a été écrit ainsi. Nous expliquons comment utiliser l'ADR (Architecture Decision Record) — un fichier M...
Migrer une application Windows vers le Web : les cas à éviter — tableau de décision et la solution réaliste du « fractionnement »
Les demandes de migration d'applications Windows internes vers le Web se multiplient, mais pour les applications reposant sur l'intégrati...
Un tableau de décision pour choisir entre arrêt et poursuite après une exception inattendue
Lorsqu'une exception inattendue survient, faut-il arrêter l'application ou la laisser continuer ? Cet article organise la décision sous l...
Liste de contrôle minimale de sécurité pour le développement d'applications Windows
Pour les applications métier WPF / WinForms / WinUI / C++ / C#, cet article organise sous forme de liste de contrôle les bases concernant...
Bien choisir entre les 3 minuteurs .NET - PeriodicTimer/Timer/DispatcherTimer
Cet article explique les différences entre PeriodicTimer, System.Threading.Timer et DispatcherTimer, et comment bien les choisir selon qu...
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.
Thread UI et minuteries
Thread UI WPF / WinForms, flux asynchrones, Dispatcher et temporisation.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Conseil technique et revue de conception
Ce sujet couvre la conception des attentes, le choix des primitives de synchronisation et les compromis latence/consommation dans les systèmes soft real-time, ce qui se prête bien au conseil technique et aux revues de conception.
Développement d'applications Windows
Remplacer le polling par timer par une conception pilotée par événements dans les applications et services Windows a un impact direct sur la qualité d'implémentation, un thème central du développement d'applications Windows.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Pourquoi Sleep(1) ne se réveille-t-il pas exactement 1 milliseconde plus tard sous Windows ?
- Les attentes chronométrées sous Windows sont limitées par la granularité de l'horloge système, et avec les réglages courants, la résolution du timer de la plateforme est de l'ordre de 15,6 ms. Ainsi, même en spécifiant 1 ms, l'attente réelle est souvent bien plus grossière. De plus, quand le délai s'écoule, le thread devient simplement prêt à s'exécuter ; rien ne garantit qu'il obtienne le CPU immédiatement, car l'ordonnancement est influencé par les autres threads, les priorités, les états d'inactivité du CPU et les DPC/ISR.
- Quand faut-il utiliser une attente sur événement plutôt qu'un timer sous Windows ?
- Utilisez une attente sur événement dès que ce que vous attendez réellement est un événement plutôt qu'un point dans le temps : un travail qui arrive dans une file, une E/S qui se termine, une demande d'arrêt, ou un changement de valeur. Le côté où l'événement se produit signale, et le côté qui attend se réveille avec une raison déjà présente, ce qui est préférable pour la latence, le CPU et la consommation. Réservez les timers aux cas où le temps lui-même est réellement la condition, comme l'envoi de métriques toutes les 5 secondes ou une nouvelle tentative après 200 ms.
- Quel est le problème avec le fait de scruter une file avec Sleep(1) ou Task.Delay(1) ?
- Ce schéma pose trois problèmes : le thread se réveille périodiquement même quand la file est vide, la latence est tirée par la granularité du timer, et cela gaspille de l'énergie. La solution consiste à faire signaler un événement par le producteur immédiatement après l'ajout dans la file, tandis que le consommateur attend avec WaitForSingleObject ou WaitForMultipleObjects et vide la file à son réveil. En C#, une boucle async avec Task.Delay(1) paraît douce en apparence mais reste du polling dans son essence.
- Faut-il utiliser timeBeginPeriod(1) pour rendre les attentes courtes plus précises ?
- Ce ne devrait pas être votre premier choix par défaut, pour trois raisons : cela a un coût en énergie et en performance, son comportement sur les versions récentes de Windows est devenu plus complexe, et le fait d'en avoir besoin signifie souvent que la cause racine n'a pas été corrigée. Si ce que vous attendez est réellement un événement, basculez plutôt vers une attente sur événement. Si vous avez véritablement besoin d'attendre un point dans le temps, un waitable timer exprime l'intention plus clairement qu'un empilement de Sleep.
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.
Liens publics