Help us understand the problem. What is going on with this article?

[node.js]スレッドへの引数の渡し方

非同期の動作をさせたくて、スレッド作成の方法を勉強しましたが、
引数の渡し方についてあまり情報がなかったので書き留めます。

javascriptにおいてスレッドという呼び方が正しいのかはまだわからないですが
(基本的にシングルスレッドで動作するものなので、どういう仕組みかは厳密にはまだ不明なためです)
動作させてみたところ非同期に動作しているように見えました。

環境

macOS : 10.15.5 Catalina
node.js : v14.3.0

スレッド呼び出しの種類について

非同期スレッドの作り方は二種類あり、①クラスタ②子プロセス呼び出し
です。簡単に以下にそれぞれの使い方を書きます。
①は呼び出し元のjsファイルが複数個同時に動作するイメージです
②は呼び出し元とは異なるjsファイルが同時に動作するイメージです。

クラスタ呼び出し

cluster.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ファイル名 , 引数配列 );

parent.js
var child_process = require('child_process');
var c = child_process.fork(__dirname+'/child' , ['foo' , 'bar' ] );

呼ばれる子プロセスは、
process.argv配列に、親から指示された引数が格納されています。
以下の例は全部の引数要素をダンプします。

child.js
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()が送信処理。

parent.js
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');
child.js
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]

ためしにたくさん子プロセスを呼び出してみた。

これは参考ですが、たくさん呼び出すとどうなるものかを試してみました。

parent.js
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'); });
child.js
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

もっとたくさん呼び出してみた。

上記コードの子プロセス数をたくさんにしてみました。

parent.js
   :
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自身が飛んだりということはありませんでした。
過剰に呼び出しすぎるのはよくないですが、もしかしたら時間を開けて子プロセスの起動を行えば、問題がなくなるかもしれません。
(すべて想像です)

naoy109
組み込み技術者です。普段は主にc,c++やアセンブラなどを扱っています。 昨今ではITの技術の進歩が目覚しく、最先端のソフトウェア開発環境に触れてみたくITの勉強をはじめました。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした