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 tasks
and 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