`1ms` : La Fausse Promesse de `sleep()` pour le Code Temps Réel — Résolution et Jitter Réparées

1ms : La Fausse Promesse de sleep() pour le Code Temps Réel — Résolution et Jitter Réparées

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.

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.

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 comme select() ou poll().

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.

Articles Connexes

Règles de Codage de Rust et Ressources Pédagogiques

Règles de Codage de Rust et Ressources Pédagogiques

Quel style de codage dois-je adopter pour mon code Rust afin d’assurer la cohérence avec mes bibliothèques Rust préférées ? Où puis-je apprendre à développer une application spécifique en Rust et quelles bibliothèques dois-je utiliser ? Comment puis-je tirer le meilleur parti des outils de développement Rust ?

Lire la suite
Enums vs Constantes : Pourquoi les Enums sont une solution plus sûre et plus intelligente

Enums vs Constantes : Pourquoi les Enums sont une solution plus sûre et plus intelligente

Utilisez-vous toujours des entiers ou des chaînes de caractères pour représenter des catégories fixes dans votre code ? Si c’est le cas, vous risquez d’introduire des bugs. Les énumérations (Enums) offrent une sécurité au moment de la compilation, garantissant que les valeurs sont valides avant même que vous n’exécutiez votre code.

Lire la suite