En bref, les appels à timeBeginPeriod à partir d'un processus affectent désormais les autres processus moins qu'avant, bien que l'effet soit toujours présent.
Je pense que le nouveau comportement est essentiellement une amélioration, mais c'est étrange et mérite d'être documenté. Honnêtement, je vous préviens - je n'ai que les résultats de mes propres expériences, donc je ne peux que deviner les objectifs et certains effets secondaires de ce changement. Si l'une de mes conclusions est incorrecte, veuillez me le faire savoir.
Les interruptions de la minuterie et leur raison d'être

Sleep (1);
Il vaut la peine de considérer comment cela est mis en œuvre. Idéalement, lorsque Sleep (1) est appelé, le processeur se met en veille. Mais comment le système d'exploitation réveille-t-il le thread si le processeur est en veille? La réponse est les interruptions matérielles. Le système d'exploitation programme une puce - une minuterie matérielle qui déclenche ensuite une interruption qui réveille le processeur et le système d'exploitation démarre ensuite votre thread.
Les fonctions WaitForSingleObject et WaitForMultipleObjects ont également des valeurs de délai d'expiration, et ces délais sont implémentés à l'aide du même mécanisme.
S'il y a beaucoup de threads en attente de minuteries, le système d'exploitation peut programmer une minuterie matérielle pour une heure individuelle pour chaque thread, mais cela conduit généralement au fait que les threads se réveillent à des moments aléatoires et que le processeur ne dort pas normalement. L'efficacité énergétique du processeur dépend fortement de son temps de veille ( le temps normal est de 8 ms ), et les réveils aléatoires n'y contribuent pas. Si plusieurs threads synchronisent ou regroupent leurs attentes en matière de minuterie, le système devient plus économe en énergie.
Il existe de nombreuses façons de combiner les réveils, mais le mécanisme principal de Windows consiste à interrompre globalement un chronomètre à un rythme constant. Lorsqu'un thread appelle Sleep (n) , le système d'exploitation programme le thread pour démarrer immédiatement après la première interruption du minuteur. Cela signifie que le thread peut finir par se réveiller un peu plus tard, mais Windows n'est pas un système d'exploitation en temps réel, il ne garantit pas du tout une heure de réveil spécifique (à ce moment, les cœurs du processeur peuvent être occupés), il est donc tout à fait normal de se réveiller un peu plus tard.
L'intervalle entre les interruptions de la minuterie dépend de la version de Windows et du matériel, mais sur toutes mes machines, il est par défaut de 15,625 ms (1000 ms / 64). Cela signifie que si vous appelez Sleep (1)à un moment aléatoire, le processus sera réveillé quelque part entre 1,0 ms et 16,625 ms dans le futur lorsque la prochaine interruption de la minuterie globale sera déclenchée (ou une fois plus tard s'il est trop tôt).
En bref, la nature des retards de minuterie est telle que (à moins que l'attente active du processeur ne soit utilisée, et veuillez ne pas l'utiliser ), le système d'exploitation ne peut réveiller les threads qu'à certains moments en utilisant des interruptions de la minuterie, et Windows utilise des interruptions régulières.
Certains programmes ne prennent pas en charge un si large éventail de latences (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, de nombreux jeux, etc.). Heureusement, ils peuvent résoudre le problème avec la fonction timeBeginPeriodce qui permet au programme de demander un intervalle plus petit. Il existe également une fonction NtSetTimerResolution qui permet de définir l'intervalle à moins d'une milliseconde, mais elle est rarement utilisée et jamais nécessaire, je ne le mentionnerai donc pas à nouveau.
Des décennies de folie
Voici une chose folle: timeBeginPeriod peut être appelé par n'importe quel programme et il modifie l'intervalle d'interruption du minuteur, et l'interruption du minuteur est une ressource globale.
Imaginez que le processus A est dans une boucle avec un appel à Sleep (1) . C'est faux, mais c'est le cas, et par défaut, il se réveille toutes les 15,625 ms, soit 64 fois par seconde. Ensuite, le processus B entre et appelle timeBeginPeriod (2) . Cela provoque le déclenchement de la minuterie plus souvent et tout à coup le processus A se réveille 500 fois par seconde au lieu de 64 fois par seconde. C'est de la folie! Mais c'est ainsi que Windows a toujours fonctionné.
À ce stade, si le processus C est apparu et appelé timeBeginPeriod (4), cela ne changerait rien - le processus A continuerait de se réveiller 500 fois par seconde. Dans une telle situation, ce n'est pas le dernier appel qui définit les règles, mais l'appel avec l'intervalle minimum.
Ainsi, un appel à timeBeginPeriod depuis n'importe quel programme en cours d'exécution peut définir l'intervalle d'interruption de la minuterie globale. Si ce programme se ferme ou appelle timeEndPeriod , le nouveau minimum prend effet. Si un programme appelle timeBeginPeriod (1) , il s'agit désormais de l'intervalle d'interruption du minuteur à l'échelle du système. Si un programme appelle timeBeginPeriod (1) et un autre appelle timeBeginPeriod (4) , alors l'intervalle d'interruption du minuteur d'une milliseconde devient une loi universelle.

Le navigateur Web est une application qui nécessite une planification basée sur la minuterie. Le standard JavaScript a une fonction setTimeout qui demande au navigateur d'appeler une fonction JavaScript après quelques millisecondes. Chromium utilise des minuteries pour implémenter cela et d'autres fonctionnalités (essentiellement WaitForSingleObject avec des délais d'attente, pas Sleep). Cela nécessite souvent un taux d'interruption de minuterie accru. Pour maintenir la durée de vie de la batterie faible, Chromium a été récemment repensé pour maintenir sa fréquence d'interruption du minuteur en dessous de 125 Hz (intervalle de 8 ms) sur batterie .
timeGetTime
La fonction timeGetTime (à ne pas confondre avec GetTickCount) renvoie l'heure actuelle, mise à jour par une interruption du minuteur. Historiquement, les processeurs n'ont pas été très doués pour garder une heure précise (leurs horloges sont délibérément fluctuées pour éviter de servir d'émetteurs FM, et pour d'autres raisons), de sorte que les processeurs comptent souvent sur des générateurs d'horloge séparés pour maintenir une heure précise. La lecture à partir de ces puces est coûteuse, c'est pourquoi Windows maintient un compteur de millisecondes de 64 bits mis à jour avec une interruption du minuteur. Cette minuterie est stockée dans la mémoire partagée, de sorte que tout processus peut lire à bas prix l'heure actuelle sans avoir à passer à l'horloge. timeGetTime appelle ReadInterruptTick , qui lit simplement ce compteur 64 bits. C'est si simple!
Puisque le compteur est mis à jour par l'interruption du minuteur, nous pouvons le suivre et trouver la fréquence d'interruption du minuteur.
Nouvelle réalité non documentée
Avec la sortie de Windows 10 2004 (avril 2020), certains de ces mécanismes ont légèrement changé, mais d'une manière très déroutante. Premièrement, il y avait des messages indiquant que timeBeginPeriod ne fonctionnait plus . En fait, tout s'est avéré beaucoup plus compliqué.
Les premières expériences ont donné des résultats mitigés. Lorsque j'ai exécuté le programme avec un appel à timeBeginPeriod (2) , clockres a montré un intervalle de minuterie de 2,0 ms , mais un programme de test séparé avec une boucle Sleep (1) s'est réveillé environ 64 fois par seconde au lieu de 500 fois comme dans les versions précédentes de Windows.
Expérimentation scientifique
Ensuite, j'ai écrit quelques programmes pour étudier le comportement du système. Un programme ( change_interval.cpp ) se trouve juste dans une boucle, appelant timeBeginPeriod à des intervalles de 1 à 15 ms . Elle maintient chaque intervalle pendant quatre secondes, puis passe au suivant, et ainsi de suite en cercle. Quinze lignes de code. Facile.
Un autre programme ( measure_interval.cpp ) exécute plusieurs tests pour vérifier comment son comportement change lorsque change_interval.cpp change. Le programme surveille trois paramètres.
- Elle demande au système d'exploitation quelle est la résolution actuelle du minuteur global en utilisant NtQueryTimerResolution .
- timeGetTime, , — , .
- Sleep(1), . .
@FelixPetriconi a exécuté des tests pour moi sur Windows 10 1909 et j'ai exécuté des tests sur Windows 10 2004. Voici les résultats sans gigue :

Cela signifie que timeBeginPeriod définit toujours l'intervalle de minuterie global sur toutes les versions de Windows. À partir des résultats de timeGetTime () , on peut dire que l'interruption est déclenchée à ce rythme sur au moins un cœur de processeur, et l'heure est mise à jour. Notez également que 2.0 dans la première ligne pour 1909 était également 2.0 dans Windows XP , puis 1.0 dans Windows 7/8, et puis il semble être revenu à nouveau à 2.0?
Cependant, le comportement de planification change considérablement dans Windows 10 2004. Auparavant, le délai de veille (1)dans n'importe quel processus équivaut simplement à l'intervalle d'interruption du minuteur, à l'exception de timeBeginPeriod (1), ce qui donne un graphique comme celui-ci:

dans Windows 10 2004, la relation entre timeBeginPeriod et la latence du sommeil dans un autre processus (qui n'a pas appelé timeBeginPeriod ) semble étrange:

la forme exacte du côté gauche du graphique n'est pas claire, mais ça va définitivement dans le sens opposé du précédent!
Pourquoi?
Effets
Comme indiqué dans la discussion sur reddit et hacker-news, il est probable que la moitié gauche du graphique soit une tentative d'imiter le plus fidèlement possible la latence «normale», étant donné la précision disponible de l'interruption du temporisateur global. C'est-à-dire qu'avec un intervalle d'interruption de 6 millisecondes, le retard se produit d'environ 12 ms (deux cycles) et avec un intervalle d'interruption de 7 millisecondes, le retard se produit d'environ 14 ms (deux cycles). Cependant, mesurer les retards réels montre que la réalité est encore plus déroutante. Avec une interruption de minuterie réglée à 7 ms, une latence Sleep (1) de 14 ms n'est même pas le résultat le plus courant:

certains lecteurs peuvent blâmer le bruit aléatoire sur le système, mais lorsque le taux d'interruption de la minuterie est de 9 ms ou plus, le bruit est nul, donc ce n'est pas le cas. pourrait être une explication.Essayez d'exécuter vous-même le code mis à jour . Les intervalles d'interruption de la minuterie de 4 ms à 8 ms semblent particulièrement controversés. Les mesures d'intervalle doivent probablement être effectuées à l'aide de QueryPerformanceCounter car le code actuel est affecté de manière aléatoire par les modifications des règles de planification et les modifications de la précision du minuteur.
Tout cela est très étrange et je ne comprends pas la logique ou la mise en œuvre. C'est peut-être une erreur, mais j'en doute. Je pense qu'il y a une logique complexe de rétrocompatibilité derrière cela. Mais le moyen le plus efficace d'éviter les problèmes de compatibilité est de documenter les modifications, de préférence à l'avance, et ici les modifications sont effectuées sans préavis.
Cela n'affectera pas la plupart des programmes. Si un processus souhaite une interruption du minuteur plus rapide, il doit appeler timeBeginPeriod lui-même.... Cependant, les problèmes suivants peuvent survenir:
- Le programme peut supposer accidentellement que Sleep (1) et timeGetTime ont la même résolution, ce qui n'est plus le cas. Bien qu'une telle hypothèse semble peu probable.
- , . — Windows System Timer Tool TimerResolution 1.2. «» , . , . , , .
- , , . , . . , , timeBeginPeriod , , .
Le programme de test change_interval.cpp ne fonctionne que si personne ne demande un taux d'interruption de minuterie plus élevé. Étant donné que Chrome et Visual Studio ont l'habitude de le faire, j'ai dû faire la plupart de mes expérimentations sans accès Internet et sans codage dans le bloc-notes . Quelqu'un a suggéré Emacs, mais m'impliquer dans cette discussion est hors de mon pouvoir.