JavaScript

JavaScriptで同期的にsleepする方法 (通常用途には使わないでください)

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待つ


副作用 (大変危険)

イベントループが回らないので、非同期I/Oがすべて止まります。

Promiseに依存した処理も止まります。

NodeのAPIも、どれが動いてどれが動かないか確実なことは言えません。

ただ、システムコールの薄いラッパーになっているAPIは動作すると考えられます。

Node 10,11 の環境で、 fs.readSync fs.writeSync process.stdout.cursorTo process.stdout.clearScreenDown が動作することは確認できました。


結論

軽い気持ちで使うと、開発チームメンバーからしばかれること必至です。

普通のsleep目的で使ってはいけません。