Node.jsのEvent LoopとSync系メソッド
execとexecSync
Node.jsのexecSync等のSync系メソッドは非Sync系のexecメソッド等と比べて可読性や書き味が良いと思います。
例えばexecSyncでOSのechoを実行する場合は以下のように書けます。
import { execSync } from 'child_process'
const reslut = execSync('echo hello')
console.log(reslut.toString()) // helloが表示される
しかしexecの場合は以下のようにcallback形式で結果を受け取る必要があり、若干見通しが悪いです。
(stdoutの結果を利用して色々やりたい時に、どんどんcallbackのネストが深くなったりします)
import { exec } from 'child_process'
exec('echo hello', (error, stdout, stderr) => {
console.log(stdout) // helloが表示される
})
promisifyとexecの併用
promisifyを利用すればexecSyncのように、上から下に見通し良く書けるようになりますが、
わざわざpromisifyを併用したり、execa等のパッケージを利用するのが大袈裟な時もあります。
import { promisify } from 'util'
import { exec } from 'child_process'
const execPromisified = promisify(exec)
const result = await execPromisified('echo hello')
console.log(result.stdout) // helloが表示される
execSyncの注意点
こういった理由からexecSyncで問題ない箇所はexecSyncを利用することがありますが、以下のようなケースで思わぬ副作用があったたためメモを兼ねて紹介します。
以下は良くあるPromise.allを利用した並列処理で、ぱっと見はsleep 1を同時に10並列走らせるため1秒程で完了するように見えます。
ですが、実際は完了まで10秒程かかります。
import { execSync } from 'child_process'
const sleepMany = async () => {
await Promise.all(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(() => execSync('sleep 1')),
)
}
execSyncとEvent Loop
この理由はexecSyncはNode.jsのEvent Loopをブロックしてしまうためで、公式ドキュメントにもしっかり以下の説明があります。
Synchronous process creation
The child_process.spawnSync(), child_process.execSync (), and child_process.execFileSync() methods are
synchronous and will block the Node.js event loop, pausing execution of any additional code until the spawned process exits.
ref: https://nodejs.org/api/child_process.html#child_processexecsynccommand-options
execSyncの用途
なおexecSyncがどのような用途に適しているのかも、公式ドキュメントで丁寧に紹介されています
Blocking calls like these are mostly useful for
simplifying general-purpose scripting tasksand for simplifying the loading/processing ofapplication configuration at startup.
ref: https://nodejs.org/api/child_process.html#child_processexecsynccommand-options
補足
今回の例に遭遇したきっかけは、元々は単発での利用が想定されていた以下のような関数を、
const someSimpleFn = () => execSync('sleep 1')
以下のようにより上位の複雑な関数に組み込んだ際に気付いたものでした。
const someOuterComplexFn = async () => {
await Promise.all(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(() => someSimpleFn()),
)
}
公式の説明のような箇所でexecSyncを使うのは良さそうですが、複数メンバで開発していたり、将来関数が別の箇所や方法で呼ばれる可能性がある場合は最初からexecを利用した方が無難かもしれません。
Discussion