Was denn nun SwitchToThread(), Sleep(0), Sleep(1)?

Was macht man, wenn man keine Wait-Funktionen verwenden will, aber dennoch möchte, dass ein anderer Thread weiterarbeiten kann. Zum Beispiel, weil man einen Spinlock implementieren will.

Nun es gibt insgesamt vier Methoden die durch das Netz geistern.
Ich gehe mal der Häufigkeit nach, die so in manchen Code-Samples finde:

1. __noop;

Wenn der Lock kurz ist, scheint es das beste zu sein, einfach die Schleife weiterlaufen zu lassen und zu hoffen, dass der ein Thread auf einem anderen Prozessor, die Ressource freigibt. Das eignet sich wirklich nur, wenn die Zeitdauer der Sperre als extrem kurz anzusehen ist und eine hohe Anzahl von Prozessoren zur Verfügung steht.
Nach allen Test, die ich gemacht habe, sollte man aber von dieser Art des Wartens bei einem Spinlock absehen. Es schiebt die Leistung des Kerns auf 100% und bringt nichts.

2.  Sleep(0);

Lies sich gut. Schlafe aber eben nicht lange. Man hat auch schon irgendwo gelesen, dass durch diese Methode der Rest der Zeitscheibe dieses Threads aufgegeben wird und ein anderer Thread an die Reihe kommt.
Leider stimmt das nicht ganz ❗
Liest man die Doku genau steht da aber:

A value of zero causes the thread to relinquish the remainder of its time slice to any other thread of equal priority that is ready to run. If there are no other threads of equal priority ready to run, the function returns immediately, and the thread continues execution.

😮 Threads mit höherer oder niedriger Prio haben also nichts davon.

Besonders eklig wird das ganze gerade wenn man Threads unterschiedlicher Prio hat, die hier gegeneinander laufen. Sleep(0); führt in diesem Fall zu einerunnötigen Prozessorlast und eben nicht dazu, dass die Zeitscheibe abgegeben wird. Der Prozess kommt sofort wieder an die Reihe und spin-t weiter.

3. SwitchToThread();

OK. Seit Windows 2000 gibt es diese nette neue Funktion. Damit wird ein anderer Thread aktiv. Egal was für eine Prio er hat. Aber auch diese Funktion tut evtl. nicht genau das was man will.
Auch hier stecken die Tücken im Detail der Doku:

The yield of execution is limited to the processor of the calling thread. The operating system will not switch execution to another processor, even if that processor is idle or is running a thread of lower priority.

Sollte also der Thread, auf den man wartet auf dem anderen Prozessor laufen, so profitiert der nicht von dem Aufruf von SwitchToThread.

4. Sleep(1):

Hiermit erreicht man wirklich was man möchte. Man gibt seine Timeslice auf und erstmal sind die anderen dran.

Mein persönliches Fazit:

Nach meinen Recherchen ist Sleep(1); der vernünftigste Weg seine Zeitscheibe abzugeben. Und nach meinem Dafürhalten ist ein __noop; strickt zu vermeiden. Die Performance ist grottenschlecht.
Das ganze Verhalten hängt extrem auch von den Umständen ab: Zahl der Theads, Häufigkeit der Kollision, Anzahl verfügbare Prozessoren, Verteilung der Prioritäten, Allgemeine Belastung des Systems, Zeitdauer der Sperre etc.

Ich habe mit einigen Parametern gespielt und auch ein kleines Sample gebaut, dass alle 4 oben genannten Funktionen durchprobiert und in dem man auch mit anderen Faktoren (Priorität etc.) spielen kann.
Es zeigte sich, dass Sleep(1); am effektivsten war. Aber dicht auf gefolgt von Sleep(0);, was mich doch etwas überraschte.

Allerdings führen schon kleinste Änderungen (Lockdauer, Zahl der Prozessoren, Spielen mit der Priorität) zu anderen Ergebnissen.
Interessant ist vor allem das Spielen mit den Prioritäten. Man soll nicht glauben, das ein Thread selbst mit niedrigster Prio noch relativ häufig Arbeit bekommt.

Viel Spaß beim Spielen mit dem Code SleepTest

7 Gedanken zu „Was denn nun SwitchToThread(), Sleep(0), Sleep(1)?“

  1. Naja, zu 2. kann ich Dir nicht ganz zustimmen; vielleicht ist die Doku auch etwas „ungenau“. Genauer wäre: Höher oder gleich…
    Was bei SLeep(0) genau passiert ist: Der Thread wird einfach in die Ready-To-Run Queue ganz hinten eingefügt. Aber dann führt der Scheduler seine normale arbeit aus. D.h. wenn ein höher priorer Thread Ready-To-Run ist, kommt natürlich der dran und nicht der gerade aktuelle. Es bringt also imer was für „gleiche und höhere“!

  2. … und zu 4: Wenn man einen Spin-Lock implementieren iwll ist dies IMHO die schlechteste Variante, da dies bei einem „normalen“ System zu einer Wartezeit von ca. 10-15 ms führt, was man dann nicht als „Spin-Lock“ Bezeichnen kann. Dann lieber „normale“ Wait-Funktionen, die sind *wesentlich* schneller!

    Also, ein Spin-Lock lässt sich IMHO nur wie while(condition); implementieren 😉

  3. Ist schon ein bisschen her, aber als Ergänzung zum letzten Kommentar: An dieser Stelle ist __noop in der Schleife genau richtig, da eine leere Schleife ratzfatz vom Compiler entsorgt wird.

  4. Der Kommentar von Jochen. 🙂 Ich rede vom Spin-Lock zur Synchronisation mit verschiedenen Kernen bei sehr kurzen erwarteten Arbeitslasten (als Vorlauf zum Yield oder Lock). An dieser Stelle will man wirklich keinen system call, weil der eben einfach Zeit kostet – bei Sleeps und Locks systembedingt mindestens in Größenordnung eines Jiffies. Dass man einen Spin-Lock selbst implementiert und dafür gute Gründe hat, ist allerdings eher die Ausnahme.

    Hinsichtlich der Synchronisation mit anderen Threads, bei denen es um tendenziell längere Arbeitspakete geht, gebe ich Recht; Der klassische Hinweis, dass man anstelle von Sleeps in vielen Fällen mit Mutexen/Semaphoren systematisch besser beraten ist gilt wie immer (wenngleich auch das natürlich system calls sind), aber das ist freilich ein anderes Thema.

    Ich finde den Hinweis über Sleep(1) gegenüber Sleep(0) sehr angebracht, übrigens, und danke dafür. Ein Kollege hatte das Problem in einer sehr engen Verarbeitungsschleife für UDP-Pakete aus einem Finanzdatenstrom, die so viel Zeit gelutscht hat, dass andere Threads kaum Zeit fanden. Er hatte es mit Sleep(0) gelöst, weswegen ich überhaupt erst auf das Thema kam. (Allerdings hatten wir auch keine aktive Threadpriorisierung vorgenommen … vielleicht Glück gehabt.)

  5. Den Einwänden gegen die Interpretation von Sleep(0) sowie die Schlussfolgerung, dass Sleep(1) vorzuziehen ist, kann ich mich nur anschliessen. Hier steht leider viel Falsches.

    Erstens: Die Aussage „Threads mit höherer Priorität profitieren nicht von Sleep(0)“ zeugt von Unverständnis, wie der Windows Scheduler funktioniert. Der Windows Scheduler ist nicht fair. Ein Thread höherer Priorität, der „ready to run“ wird, unterbricht einen Thread mit niedrigerer Priorität auch inmitten der Zeitscheibe, vollkommen unabhängig davon, was der andere Thread macht. Threads mit höherer Priorität können andere Threads ohne weiteres komplett verhungern lassen.
    Es ist insofern zwar technisch richtig, dass Threads mit höherer Priorität nicht profitieren, aber das ist komplett unwichtig (weil es die eben einfach nicht kümmert). Berücksichtigen muss man übrigens auch noch den Priority-Boost, den ein Thread mit gleicher Priorität für 2 Zeitscheiben erhält, sobald ein Event signalisiert bzw. eine blockierende I/O Aktivität beendet wurde. Damit kann auch ein Thread gleicher Priorität jederzeit den laufenden Thread unterbrechen. Sleep(0) gibt alle anderen Threads, die gleiche Priorität haben, eine Chance. Das ist i.d.R. akzeptabel, aber nicht immer die beste Lösung.

    Zweitens: SwitchToThread funktioniert nur für einen anderen Thread, der für den selben Prozessorkern geplant ist, das ist schon richtig. Falsch ist aber, dass das eine schlechte Sache ist, ganz im Gegenteil. Einen Thread auf einen anderen Kern umzuziehen ist wegen Cache und TLB (und eventuell NUMA) eine sehr haarige Sache, das macht ein Scheduler nur ausgesprochen ungern und selten (wenn genug Zeit vergangen ist, dass es wahrscheinlich nicht schadet, i.d.R. mindestens mehrere hundert Millisekunden). Bei jedem Aufruf, das wäre reiner Wahnsinn.

    Drittens: Wait(1) wartet mindestens eine Millisekunde, bei Kernels vor Windows 10 gerundet auf das Scheduler Intervall (15,6ms), und das Runden (aufwärts/abwärts) is abhängig von der genauen Windows Version.
    Zudem ist „warten“ hier der falsche Ausdruck. Richtiger ist: „nicht im ready state sein“. Was ist der Unterschied? Der Unterschied ist, dass nach Ablauf der Nicht-Null Zeit lediglich der Thread wieder „ready“ ist, d.h. er könnte irgendwann mal laufen, wenn es eben passt.
    Praktisch bedeutet das, dass Wait(1) je nach Windows Version und NtSetTimerResolution/timeBeginPeriod häufig zwischen 1ms 16ms, dabei zu 99% konsistent verzögert, aber eben in 1% der Fälle durchaus auch mal 50-60ms, je nachdem, wie die Timer laufen und wie der Thread gescheduled wird. Das ist für die meisten Anwendungen inakzeptabel.

    Viertens: __noop (oder _mm_pause()) in einer „Busy wait“ Schleife ist wichtig, nicht nur wegen des Optimizers und nicht nur deshalb weil es bei bestimmten Xenons den exzessiven Energieverbrauch senkt (sprich: Feuer vermieden werden kann). Wichtiger ist, dass es generell bei allen HT-fähigen Prozessoren (auch Desktop und mobile) der CPU signalisiert, dass sie die gemeinsamen Ressourcen jetzt komplett für den anderen virtuellen Kern verwenden kann, weil dieser Thread hier gerade nichts Sinnvolles tut. Damit erhält ein Thread, der vielleicht gerade auf dieser CPU läuft, überhaupt die Chance, das Lock freizugeben.

    Für die meisten Anwendungen ist SwitchToThread genau richtig, alternativ Sleep(0), sofern es OK ist, wenn ein Thread umzieht. Beziehungsweise bei kurzer Lock-Zeit eben ein Busy-Wait, aber bitte unbedingt mit PAUSE.

    1. Danke für Deinen Kommentar. Sicher kommt es auf den Bereich an! Aber hast Du mein Sample mal angeschaut und ausprobiert. Hier ein aktuelle Run auf einem Windows 10 1607.

      noop;
      Count 1: 3372299
      Count 2: 3236544
      Count 3: 3391157
      Duration: 15984
      Sleep(0);
      Count 1: 3338999
      Count 2: 3334147
      Count 3: 3326854
      Duration: 4094
      SwitchToThread();
      Count 1: 3368211
      Count 2: 3343572
      Count 3: 3288217
      Duration: 4531
      Sleep(1);
      Count 1: 3096880
      Count 2: 3068374
      Count 3: 3834746
      Duration: 2250

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

This site uses Akismet to reduce spam. Learn how your comment data is processed.