JavaScript: Wie legt man eine Pause ein (sleep)?
JavaScript hat kein blockierendes sleep, und das mit Absicht: Ein Thread führt
deinen Code aus, die Timer und die Klicks — ein Thread, der schläft, hält also
alle drei an.
Das, was man nimmt
const sleep = d => new Promise(r => setTimeout(r, d));
await sleep(300);
await sleep(300) took 301 ms
setTimeout
in ein
Promise
gepackt und darauf gewartet. Drei Tokens, keine Abhängigkeit.
Was es tatsächlich anhält
after starting slow(), we are already here at 0 ms
slow done at 200 ms
Nur die async-Funktion, in der es steht.
await
gibt an die Event-Loop ab, alles andere läuft also weiter — Timer feuern, die
Seite bleibt bedienbar, andere abgewartete Arbeit kommt voran. Das ist der
Unterschied zu einem sleep() in einer Sprache mit Threads, und es ist ein
Feature.
Ohne await hält setTimeout gar nichts an:
the line after setTimeout runs at 0 ms
timer fired at 100 ms
Was ein Busy-Wait kostet
function busySleep(d) { const end = Date.now() + d; while (Date.now() < end) {} }
busySleep(300) took 300 ms
did the 50 ms timer fire during it? false
after yielding, has it fired now? true
Die Schleife blockiert tatsächlich, deshalb greift man danach. Der Preis steht in der zweiten Zeile: Ein Timer, der nach 50 ms fällig war, lief erst nach der Schleife, 250 ms zu spät. Auch sonst konnte nichts laufen — im Browser heißt das eingefrorene Seite, kein Scrollen, keine Klicks, und nach ein paar Sekunden ein “Seite reagiert nicht”-Dialog. Nebenbei brennt es die ganze Zeit einen Kern.
Die Verzögerung ist ein Minimum
sleep(0) actually took 1 ms
sleep(5) actually took 5 ms
10 chained setTimeout(...,0) took 12 ms, not 0
setTimeout plant ein, es verspricht nichts. Der Callback läuft nicht früher
als nach der Verzögerung, und erst wenn die aktuelle Arbeit fertig ist. Eine
Verzögerung von 0 wird in Node auf 1 ms angehoben; Browser heben zusätzlich auf
4 ms an, sobald Timer mehr als fünf Ebenen tief verschachtelt sind.
Nacheinander oder parallel ist deine Entscheidung
for..of with await : 301 ms (sequential)
map + Promise.all : 100 ms (parallel)
forEach with async : 1 ms <- did not wait at all
Dreimal 100 ms warten, drei Ergebnisse. for..of mit await wartet jedes Mal.
Promise.all
startet alle drei und wartet einmal.
Die dritte Zeile ist der Fehler, den man kennen sollte.
forEach
ruft deinen async-Callback auf, bekommt ein Promise zurück und wirft es weg —
es weiß nichts damit anzufangen. Die Schleife ist also sofort fertig, und der
Code danach läuft, während die Arbeit noch offen ist. Das sieht exakt aus wie
die funktionierende Variante.
Die, die man vielleicht nicht kennt
timers/promises setTimeout(200) took 200 ms
Node bringt einen Promise-basierten Timer in
node:timers/promises
mit, auf dem Server brauchst du den Wrapper also nicht zu schreiben.
Atomics.wait(..., 200) blocked for 200 ms
Atomics.wait
blockiert den Thread wirklich, und anders als die Schleife verbraucht es dabei
keine CPU. Im Haupt-Thread eines Browsers ist es nicht erlaubt — was die
richtige Einschränkung ist und dir sagt, wo der verbleibende sinnvolle
Einsatzort liegt: in einem Worker.
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).