非同期の動作をさせたくて、スレッド作成の方法を勉強しましたが、
引数の渡し方についてあまり情報がなかったので書き留めます。
javascriptにおいてスレッドという呼び方が正しいのかはまだわからないですが
(基本的にシングルスレッドで動作するものなので、どういう仕組みかは厳密にはまだ不明なためです)
動作させてみたところ非同期に動作しているように見えました。
#環境
macOS : 10.15.5 Catalina
node.js : v14.3.0
#スレッド呼び出しの種類について
非同期スレッドの作り方は二種類あり、①クラスタ②子プロセス呼び出し
です。簡単に以下にそれぞれの使い方を書きます。
①は呼び出し元のjsファイルが複数個同時に動作するイメージです
②は呼び出し元とは異なるjsファイルが同時に動作するイメージです。
##クラスタ呼び出し
var cluster = require('cluster');
if (cluster.isMaster) { console.log('parent'); cluster.fork(); }
else { console.log('child'); }
上記コードの実行結果は以下です
parent
child
この場合、それぞれの(子)スレッドに引数を渡すことは(試した限り)出来ませんでした。
用途的には全く同じイベントハンドラを複数持つとパフォーマンスが上がる場合がよいとおもいますが、
それについてまだ知識はありません。
##子プロセス呼び出し
以下のような手順です。
引数を渡すには、fork()の第二引数にarrayを指示します。
child_process.fork( 呼び出すjsファイル名 , 引数配列 );
var child_process = require('child_process');
var c = child_process.fork(__dirname+'/child' , ['foo' , 'bar' ] );
呼ばれる子プロセスは、
process.argv配列に、親から指示された引数が格納されています。
以下の例は全部の引数要素をダンプします。
process.argv.forEach( function(item) {console.log("arg:" + item);} ); //dump args
実行の仕方と結果は以下です。
$ node parent
arg:/Users/***/.nodebrew/node/v14.3.0/bin/node
arg:/Users/***/***/child
arg:foo
arg:bar
process.argv[0] : node 実行ファイルのパス
process.argv[1] : 実行している js ファイルのパス
process.argv[2] : 引数配列 要素 [0] : 今回は親プロセスが「foo」を指示しています。
process.argv[3] : 引数配列 要素 [1] : 今回は親プロセスが「bar」を指示しています。
実際に引数を参照するには process.argv[2]〜先を読み込みするようにします。
この呼び出し方ならば、スレッド毎に親で仕事内容を指示することができます。
##親-子間の通信
子プロセスの場合には親子間でメッセージやりとりができます。
両者ともにon()が受信ハンドラ、(引数がメッセージ内容)、send()が送信処理。
var child_process = require('child_process');
var c = child_process.fork(__dirname+'/child' , ['foo' , 'bar' ] );
c.on('message', function (msg) { console.log('[Parent]received msg = [' + msg + ']'); }); //msg handler
c.send('Hello');
process.on('message', function(msg) {
console.log('[Child]received msg = [' + msg + ']' );
if(msg == 'Hello') process.send('Hello by ' + process.argv[2]);
})
実行結果は以下。
$ node parent
[Child]received msg = [Hello]
[Parent]received msg = [Hello by foo]
##ためしにたくさん子プロセスを呼び出してみた。
これは参考ですが、たくさん呼び出すとどうなるものかを試してみました。
var child_process = require('child_process');
const n_child = 16;
var _childlen =[];
for( var i = 0 ; i < n_child ; ++i) {
_childlen[i] = child_process.fork(__dirname+'/child' , [ i ] );
_childlen[i].on('message', function (msg) { console.log('[Parent]received msg = ['+ msg + ']' ); });
}
_childlen.forEach( function(a) { a.send('Hello'); });
process.on('message', function(msg) {
console.log('[Child]received msg = [' + msg + ']' );
if(msg == 'Hello') process.send('Hello by ' + process.argv[2]);
})
実行結果は以下。
parentのsend()の一連の処理と、chilldの返信処理が非同期(同時)に実行されていることがわかります。
childからのメッセージを受信したときのダンプ([Parent] received)が順不同なのが興味深いです。
イベントハンドラをどの順番に処理するかの内部的な処理によるものですが、
(あくまでも想像です)
親プロセスのイベント処理待ちキューに、順不同に入っていると思われて、
childの送信処理(16個分)が非同期に行われていることが言えるのではと思います。
単純に考えて、それぞれの子プロセスが個別のスレッドとして動作しているのならかなり効率は良いと思いました。
パフォーマンスに困ったら、これを利用して並列処理なども手段として検討できるでしょう
$node parent
Child]received msg = [Hello]
[Parent]received msg = [Hello by 0]
[Child]received msg = [Hello]
[Child]received msg = [Hello]
[Child]received msg = [Hello]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 3]
[Parent]received msg = [Hello by 5]
[Parent]received msg = [Hello by 6]
[Parent]received msg = [Hello by 1]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 4]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 2]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 7]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 11]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 8]
[Child]received msg = [Hello]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 10]
[Parent]received msg = [Hello by 12]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 9]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 14]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 13]
[Child]received msg = [Hello]
[Parent]received msg = [Hello by 15]
OSのプロセスをみてみると、やはり子ごとにプロセス(スレッド)が生成されていました。
6049 s000 S+ 0:00.01 grep node
6023 s002 S+ 0:00.20 node parent
6024 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 0
6025 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 1
6026 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 2
6027 s002 S+ 0:00.11 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 3
6028 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 4
6029 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 5
6030 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 6
6031 s002 S+ 0:00.11 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 7
6032 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 8
6033 s002 S+ 0:00.09 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 9
6034 s002 S+ 0:00.09 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 10
6035 s002 S+ 0:00.09 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 11
6036 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 12
6037 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 13
6038 s002 S+ 0:00.10 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 14
6039 s002 S+ 0:00.09 /Users/***/.nodebrew/node/v14.3.0/bin/node /Users/***/webserver_nodejs/child 15
##もっとたくさん呼び出してみた。
上記コードの子プロセス数をたくさんにしてみました。
:
const n_child = 4096;
:
結果は以下
$node parent
child : 0
child : 1
:
:
child :385
child :405
(libuv) kqueue(): Too many open files in system
/Users/***/.nodebrew/node/v14.3.0/bin/node[7040]: ../src/tracing/agent.cc:55:node::tracing::Agent::Agent(): Assertion `(uv_loop_init(&tracing_loop_)) == (0)' failed.
:
:
Error: ENFILE: file table overflow, uv_pipe_open
:
SystemErr
子プロセスの数が400個を超えたところで、エラーが表示されました。
OSのスレッド数の限界がくるのかなと思ったのですが、これはnode.jsシステムのlibuv(IO関連処理を行う部分)が扱うパイプの個数の限界のようです。
親子通信で使用しているものと思われます。
実行中、この記事を書いているブラウザも、読み込みエラーになってしまったので、OSに対しても負荷がかかったと思われます。
プロセスが多くなりすぎた原因で(各々初期化などある程度の処理が走ると思われ)
ネットワークの処理が追いつかなくなったように感じます。がmacOS自身が飛んだりということはありませんでした。
過剰に呼び出しすぎるのはよくないですが、もしかしたら時間を開けて子プロセスの起動を行えば、問題がなくなるかもしれません。
(すべて想像です)