JSはシングルスレッドなのがなあ (I/O処理等、ランタイム内で一部例外があるのは知っています)
以前からJSでスレッド使えないのかなあと思っていたのですが、2008年のこんな記事を少し前に見つけて、
JavaScriptによるマルチスレッドの実現‐Concurrent.Threadの裏側 - InfoQ
これはすごい!
と思ったのですが、実装を探すとSourceForgeにあるものの、
https://sourceforge.net/projects/jsthread/
最終コミットが4年前とかなので、これは動くか厳しいか・・・と思いました。
ダメ元で試してみる
成果物のReleaseのページから
https://sourceforge.net/projects/jsthread/files/release/
- Concurrent.Thread-full-20090713.js
- Concurrent.Thread.ScriptExecuter+Http.js
- Concurrent.Thread.Compiler+Http.js
の3つをダウンロードしてきて、以下のようなHTMLおよびJSコードを書いたらちゃんとマルチスレッドとして動きました!
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>jsthread</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script type="text/javascript" src="Concurrent.Thread.Compiler+Http.js">
</script>
<script type="text/javascript" src="Concurrent.Thread.ScriptExecuter+Http.js">
</script>
<script type="text/javascript" src="Concurrent.Thread-full-20090713.js">
</script>
</head>
<body>
<script type = "text/javascript">
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
};
function f1(){
var i = 0;
while(1){
document.body.innerHTML += i++ + "<br>";
}
};
function f2(){
while(1){
console.log(getRandomInt(1000));
}
};
Concurrent.Thread.create(f1)
Concurrent.Thread.create(f2)
</script>
</body>
</html>
書いたHTML&JSでやっていること
- よろしくスレッドに渡す関数を定義
- 2つスレッドを立ち上げる
- 1つ目のスレッドはウェブページ?に無限ループで数字を出力し続ける
- 2つ目のスレッドはコンソールに0-1000の乱数をこれまた無限ループで出力する
ってな感じです。
普通だと終了しない、かつヘビーな処理をする無限ループとか書いたら、終了のお知らせ(UI自体が固まると思われる)って感じですが、このコードはそうはなりません。
動いているところを見てみよう
repl.it に動作デモを置いておきましたにで、アクセスしてみて下さい。
https://repl.it/@ryo_grid/ConcurrentThreadtestshareqiita
Runすると動作が始ります。
Web画面では数字がダーッと出続けますが、一方でコンソールを見るとそちらでも乱数がダーッと出続けています。
async/awaitでいいのでは?
以下は私の理解で、誤っている可能性もあるので、その場合はご指摘をお願いします〇刀乙
最近のJSだとasync/awaitとかが入って、awaitしている間、他の処理が走れるので、例えばヘビーな無限ループとかあってもawaitで待つ処理でもいれてやれば、マルチスレッド的なコードが書けそうな気がするのですが、よくよく調べてみたところ、awaitかけても、その対象の処理の中を辿って行ってI/Oだとかの本当にただ待つだけの処理が存在しないと、awaitかけた行を持つasync関数はスレッドを手放さないみたいなんですよね。
なので、上でConcurrent.Threadを使って書いたような処理を並列に動作させることはおそらくできませんし、例えば、純粋に数値計算だけをする2つの関数があったとしたら、間違いなく対応不可です。
一方で、Concurrent.Threadは黒魔術を駆使して、各関数を分割して、一定のタイムスライスごとにスレッド機構(など)で言う、コンテキストスイッチをしてくれるらしいので、そのような場合でも対応することができます(現実的にそんなユースケースがあるかは別として)。
というわけでConcurrent.Threadすごい!
以上!
追記:
async/awaitでマルチスレッドっぽく書くのは無理、というようなことを書きましたが、以下の記事の冒頭にあるように、
ES2017 async/await で sleep 処理を書く
スレッドの実行権を手放すタイプのsleep関数がPromiseを使えば書けるので、そのsleep関数を定期的に呼び出してawaitするようにすれば、ノンプリエンプティブなタイプのスレッドみたいなものは実現できそうです(Fiberと呼ぶのは違うんだよな多分)。
追記2:
async/await と Promise を使って、なんちゃってマルチスレッドを書いてみました。
2つの疑似スレッドが10msごとにコンテキストスイッチ?して、コンソールに自分だ!という感じの内容の出力を行います。
処理は10秒で終了します(その前にrepl.itに打ち切られるかも)
https://repl.it/@ryo_grid/trypseudomultithreadwithasyncawait
なお、node.js の v10.13.0 でも動作することを確認しました。
全部まとめたコードも貼っておきますね。
const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
function get_unixtime_msec(){
var date = new Date() ;
var cur_date = date.getTime() ;
return cur_date;
}
async function th1(){
var start_time = get_unixtime_msec();
var past_time = get_unixtime_msec();
var cur_time = null;
while(1){
cur_time = get_unixtime_msec()
if(cur_time - past_time >= 10){ // if there is diff longer than 1 sec
if(cur_time - start_time >= 10 * 1000){
// finish this thread if 10sec elapsed
break;
}
await sleep(10);
past_time = get_unixtime_msec();
}
console.log("I am th1!!!");
}
}
async function th2(){
var start_time = get_unixtime_msec();
var past_time = get_unixtime_msec();
var cur_time = null;
while(1){
cur_time = get_unixtime_msec()
if(cur_time - past_time >= 10){ // if there is diff longer than 1 sec
if(cur_time - start_time >= 10 * 1000){
// finish this thread if 10sec elapsed
break;
}
await sleep(10);
past_time = get_unixtime_msec();
}
console.log("I am th2!!!");
}
}
async function exec_two_threads(){
th1();
await th2();
}
function success(result){
console.log("successfully two thread finished.");
}
function failed(error){
console.log(error)
console.log("thread execution is failed.");
}
// get Promise object
var two_thread_promise = exec_two_threads();
// wait finish of two threds
two_thread_promise.then(success, failed);