
1ms
: La Fausse Promesse de sleep()
pour le Code Temps Réel — Résolution et Jitter Réparées
- 13 octobre 2025
- 9 mins de lecture
- Concepts de programmation , Systemes d'exploitation , Programmation rust , Programmation python
Table des matières
Si vous avez déjà créé un système en temps réel, un simulateur de données ou une boucle de jeu, vous avez probablement essayé d’utiliser sleep()
pour contrôler le timing.
Et comme la plupart des développeurs, cela vous a trahi.
Vous vous êtes peut-être demandé : « Pourquoi mon code avec un délai de 1 ms fonctionne-t-il parfaitement sur Linux mais rame à 15 ms sur Windows ? »
La réponse est simple, subtile et cruciale pour un code fiable : la résolution du timer du système d’exploitation limite secrètement votre précision.
​
La surprise du 1 ms : pourquoi votre code tourne 15 fois plus lentement
Un développeur avait besoin d’un simple émetteur de données capable d’envoyer 1 000 enregistrements par seconde — un toutes les 1 ms. Le code Python était basique :
1import time
2
3# Objectif : 1 enregistrement toutes les 1ms
4delay_s = 0.001
5
6for i in range(1000):
7 send_data_record()
8 time.sleep(delay_s)
Résultat sous Linux : Succès ! Le script tournait à environ 1 000 enregistrements par seconde.
Résultat sous Windows : Catastrophe. Le même script est tombé à 65–70 enregistrements par seconde.
Le code n’était pas en cause : c’est le système d’exploitation qui mentait sur la durée réelle du sommeil.
​
La cause profonde : tous les sleep()
ne se valent pas
La résolution de votre appel sleep()
est plafonnée par le timer du système d’exploitation.
Même si vous demandez une microseconde, l’OS ne peut vous réveiller qu’à la prochaine « tic » de son horloge interne.
Cette distinction est cruciale pour la fiabilité multiplateforme.
​
Résolution du timer : Windows vs Linux
Système | Résolution du timer par défaut | Pourquoi c’est important |
---|---|---|
Linux | Souvent 1 ms ou mieux (jusqu’à la microseconde avec les timers haute résolution) | Quand vous demandez un sleep(1ms) , le noyau Linux respecte généralement cette durée avec précision. |
Windows | Par défaut ≈ 15,6 ms | Cette valeur grossière est un réglage d’économie d’énergie. Votre requête de 1 ms est souvent arrondie vers le haut au prochain intervalle de 15,6 ms. |
Cette valeur par défaut de 15,6 ms sur Windows est la raison pour laquelle le script Python tournait 15 fois plus lentement. Le système limitait fondamentalement la cadence de l’application.
Tip
Bien que Windows permette aux applications de demander manuellement une meilleure résolution (1 ms) via timeBeginPeriod
, cela modifie tout le système et augmente la consommation CPU et énergétique.
Évitez cette solution de contournement.
​
Le coût caché de sleep()
: des intervalles incohérents
Même avec un timer haute résolution (comme sur Linux), utiliser sleep()
naïvement reste une mauvaise pratique pour des boucles prévisibles.
Quand vous appelez sleep()
, vous dites à l’OS : « Mon thread est terminé, mets-le en pause. »
Le planificateur garantit une pause d’au moins cette durée, mais pas un réveil exactement à temps — ce qui crée du jitter (variabilité temporelle).
Le cœur du problème pour les systèmes haute fréquence se trouve ici :
​
Mauvais schéma : sleep()
fixe après le travail
1loop {
2 obtain_data(); // (Durée variable)
3 submit_data();
4 thread::sleep(Duration::from_millis(1)); // <--- Le piège
5}
Pourquoi c’est un bug : le temps total d’une itération est : (temps de traitement) + (durée de sommeil)
.
Votre intervalle sera toujours plus long que prévu, et s’il varie, le rythme devient incohérent et dérive lentement.
​
Meilleur schéma : mesurer et ajuster
La solution consiste à compenser le temps passé au traitement. Calculez le temps restant avant la prochaine itération et dormez seulement pour cette durée.
1use std::time::{Duration, Instant};
2
3let target = Duration::from_millis(1);
4
5loop {
6 let start = Instant::now();
7 obtain_data();
8 submit_data(); // Travail effectué
9
10 let elapsed = start.elapsed();
11
12 // Dormir uniquement le temps restant pour atteindre la cible de 1ms
13 if elapsed < target {
14 thread::sleep(target - elapsed);
15 }
16}
Ce modèle garantit une boucle à durée constante, rendant votre système prévisible et stable.
Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).
​
Des solutions qui fonctionnent vraiment
Abandonnez les sleep()
naïfs.
Pour du code prévisible, à faible latence et haute fréquence, adoptez ces stratégies fiables :
​
1. La référence : les timers d’intervalle
Les runtimes asynchrones modernes gèrent précisément ce problème : planification et correction de dérive automatique.
En Rust, par exemple, tokio::time::interval
corrige la dérive et garantit des intervalles constants.
1use tokio::time::{interval, Duration};
2
3let mut ticker = interval(Duration::from_micros(100)); // Intervalle cible
4
5loop {
6 ticker.tick().await; // Attend le prochain "tic" planifié
7 send_next_item(); // Traitement parfaitement cadencé
8}
C’est la méthode recommandée pour 99 % des applications haute fréquence. Le runtime gère la complexité de l’OS pour vous.
​
2. L’option nucléaire : la boucle active / spin loop ☢️
Pour les applications où la précision absolue (sous la microseconde) est indispensable — par ex. le trading à très faible latence ou le contrôle embarqué — on peut contourner totalement le scheduler de l’OS :
1use std::time::{Duration, Instant};
2use std::hint;
3
4let target = Instant::now() + Duration::from_micros(10);
5while Instant::now() < target {
6 hint::spin_loop(); // Optimise la boucle pour le CPU
7}
Warning
À utiliser avec extrême prudence : cette approche monopolise 100 % d’un cœur CPU, consomme beaucoup d’énergie et nuit aux autres tâches. À réserver aux attentes très courtes où chaque cycle compte.
​
3. L’astuce haut débit : envois groupés
Si la résolution de 1 ms est un goulet d’étranglement et que vous ne pouvez pas changer d’approche, adaptez votre logique : envoyez par lots moins souvent.
Au lieu d’envoyer un élément toutes les ~1 ms, envoyez N
éléments toutes les ~16 ms (valeur par défaut de Windows).
// Pseudocode
sleep(16ms);
send N_items; // N = nombre d'éléments à envoyer
Cela réduit la dépendance aux timers haute résolution et améliore la stabilité globale (au prix d’un peu de latence).
Note
Choisissez N
de façon à ce que la durée totale d’un cycle — sommeil (16 ms) + envoi des N
éléments — corresponde à votre cadence cible.
Par exemple, pour un espacement moyen de 1 ms, le temps total doit valoir N × 1 ms
.
​
À retenir
La précision temporelle est cruciale pour tout code sensible au timing — et sleep()
n’est souvent pas à la hauteur.
La vraie solution n’est pas un « meilleur sleep() », mais une compréhension claire de la planification par l’OS.
- N’utilisez plus
sleep()
dans les boucles. Appliquez le modèle Mesurer et ajuster. - Pour le code asynchrone, utilisez des utilitaires comme
interval
de Tokio pour corriger automatiquement la dérive. - Testez toujours votre logique temporelle sur toutes les plateformes : ce qui marche sur Linux peut échouer spectaculairement sur Windows.
Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).
​
Bonus : preuve par Python
Lancez ce petit script sous Linux et Windows pour constater la différence de résolution de timer.
Il tente de dormir 1 ms exactement 1 000 fois ; le temps total révèle la vraie résolution de sleep()
du système.
1import time
2
3def prove_sleep_resolution():
4 total_sleeps = 1000
5 sleep_duration_sec = 1 / 1000.0
6
7 start_time = time.time_ns()
8
9 for _ in range(total_sleeps):
10 time.sleep(sleep_duration_sec)
11
12 end_time = time.time_ns()
13
14 total_execution_time_ms = (end_time - start_time) / 1_000_000
15
16 print(f"Durée mesurée réelle : {total_execution_time_ms:.2f} ms")
17
18if __name__ == "__main__":
19 prove_sleep_resolution()
Résultats typiques :
Système | Temps total (ms) | Résolution observée |
---|---|---|
Linux | ~1150 ms | Haute (~1 ms), proche de la valeur attendue (1000 ms). |
Windows | ~14100 ms | Basse (~15 ms), environ 14 × plus que prévu. |
​
Annexe : plongée dans le noyau — comment sleep()
fonctionne réellement
Sous le capot, le comportement d’un appel sleep()
dépend entièrement du planificateur et des timers du système d’exploitation.
Quand vous appelez time.sleep(duration)
en Python ou en Rust, le runtime effectue un appel système vers le noyau. Voici la différence selon la plateforme :
​
Linux : timers haute résolution (mais avec du jitter)
Sous Linux, time.sleep()
se traduit souvent par l’appel système nanosleep()
.
- Résolution :
nanosleep()
peut atteindre la nanoseconde si le matériel et le noyau le permettent. - Facteur de jitter : la quantum de planification minimale du système peut toujours empêcher un réveil exact.
- Alternatives modernes : des mécanismes comme
timerfd
offrent une meilleure précision en s’intégrant à des systèmes d’E/S commeselect()
oupoll()
.
​
Windows : le timer grossier par défaut
Sous Windows, time.sleep()
repose sur un timer à résolution faible.
- But : l’efficacité énergétique. La résolution est typiquement de ~15,6 ms, permettant au CPU de dormir plus longtemps.
- Exception manuelle : il est possible de demander une résolution plus fine (1 ms), mais cela affecte tout le système — raison pour laquelle un code multiplateforme échoue sans ajustement explicite.
Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).