JavaScriptでsleepするには通常、setTimeout()
を使用します。
setTimeout(() => {
// do something after 100ms.
}, 100);
setTimeout()
に渡したコールバック関数は、JavaScriptランタイムのメッセージキューに登録されます。
setTimeout()
の呼び出し元は、コールスタックのすべての関数がreturn
することで、ランタイムに処理が戻ります。
ランタイムは、メッセージキューにすぐに実行できる関数があれば実行し、なければ待ちます。
メッセージキューには setTimeout()
で登録されたもののように、一定時間後にならなければ実行できないものと、setImmediate()
やPromise
で登録されたもののように即座に実行できるものがあります。
メッセージキューが空になると、Node.jsの環境であればプログラムが終了します。
setTimeout()のスリープで何が問題か?
setTimeout()
は、上述の通り現在のコールスタックがすべてreturn
しなければ処理が始まらないのですから、現在のコールスタックを維持しなければならない場合、sleep後に何かが呼ばれても意味がありません。
私の場合は、インタープリタを作っていて、ブレークポイントでデバッガの関数を起動し、標準入力(TTY)から入力を待つという状況で問題となりました (そのコールスタックに積まれたものをデバッグしたい)。
でも、Promise化して await で待てるんじゃないの?
確かに、すべての呼び出し元が async
となるように設計すれば可能ですが、すべての呼び出しがメッセージキュー経由となるオーバーヘッドは許容できませんし、また、同インタープリタではユーザーの任意のJavaScript関数もインタープリタの関数として組み込める機構となっているため、利便性からもNGでした。
同期的に待つ方法
Atomics.wait() - JavaScript | MDN
SharedMemory と Atomic API について
比較的新しい機能である worker の、worker間で共有メモリを利用して通信するための SharedArrayBuffer
Atomics
を使用することで、同期的にsleepすることができます。
const sab = new SharedArrayBuffer(8);
const s32ar = new Int32Array(sab);
Atomics.wait(s32ar, 0, 0, 100); // 100ms待つ
注意(2020/3/9追記)
SharedArrayBufferはSpectre脆弱性対策として一部ブラウザでは引き続き無効化されています
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
副作用 (大変危険)
イベントループが回らないので、非同期I/Oがすべて止まります。
Promiseに依存した処理も止まります。
NodeのAPIも、どれが動いてどれが動かないか確実なことは言えません。
ただ、システムコールの薄いラッパーになっているAPIは動作すると考えられます。
Node 10,11 の環境で、 fs.readSync
fs.writeSync
process.stdout.cursorTo
process.stdout.clearScreenDown
が動作することは確認できました。
結論
軽い気持ちで使うと、開発チームメンバーからしばかれること必至です。
普通のsleep目的で使ってはいけません。