Carpe Diem

備忘録

Event LoopとMicrotasksについて

概要

以前

christina04.hatenablog.com

でMacrotasksとMicrotasksについて触れました。
ただこの時使ったのがPromisesetTmeout()だけで、他の種類の非同期関数を使った場合どうなるのだろう、と気になり検証してみました。

環境

  • Node.js v8.9.0

各キューの種類と優先度

以下の図が非常に分かりやすいです。

f:id:quoll00:20180330161817p:plain

ref: Promises, Next-Ticks and Immediates— NodeJS Event Loop Part 3

Event Loop(Macrotasks)のキュー

大きく以下の3つを覚えてください

種類 実行順
Timer(setTimeout, setInterval) 1
I/O event 2
setImmediate 3

Microtasksのキュー

大きく以下の2つを覚えてください

種類 実行順
process.nextTick 1
Other(Promise etc...) 2

Event LoopとMicrotasksの順序

Event Loopの各キューはMicrotasksのキューが空になってから実行されます。

検証

Macrotasksのキューのみのケース

基本

以下のコードの場合

const fs = require('fs');
const path = require('path');

console.log('script start');

setImmediate(() => console.log('setImmediate'));
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs'));
setTimeout(() => console.log('setTimeout'), 0);

console.log('script end');

結果

以下の通りの結果になります。

script start
script end
setTimeout
fs
setImmediate

先程の実行順の通りですね。

応用1

複数キューに詰め込む場合

const fs = require('fs');
const path = require('path');

console.log('script start');

setImmediate(() => console.log('setImmediate1'));
setImmediate(() => console.log('setImmediate2'));
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs1'));
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs2'));
setTimeout(() => console.log('setTimeout1'), 0);
setTimeout(() => console.log('setTimeout2'), 0);

console.log('script end');

結果

複数でも優先度は変わりません

script start
script end
setTimeout1
setTimeout2
fs1
fs2
setImmediate1
setImmediate2

応用2

後からキューに詰め込む場合

const fs = require('fs');
const path = require('path');

console.log('script start');

setImmediate(() => {
  console.log('setImmediate')
  setTimeout(() => console.log('nested setTimeout'), 0);
});
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs'));
setTimeout(() => console.log('setTimeout'), 0);

console.log('script end');

結果

Timerのキューの方が先と言っても、後からキューに登録する場合は次のループで実行されるので当然setImmediate()より後になります。

script start
script end
setTimeout
fs
setImmediate
nested setTimeout

Microtasksのキューのみ(ほぼ)のケース

基本

console.log('script start');

Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

console.log('script end');

結果

優先度のとおりです。

script start
script end
nextTick
Promise

応用1

複数のケース

console.log('script start');

Promise.resolve().then(() => console.log('Promise1'));
Promise.resolve().then(() => console.log('Promise2'));
process.nextTick(() => console.log('nextTick1'));
process.nextTick(() => console.log('nextTick2'));

console.log('script end');

結果

複数でも優先度は変わりません。

script start
script end
nextTick1
nextTick2
Promise1
Promise2

応用2

ネストするケース。

console.log('script start');

Promise.resolve()
  .then(() => console.log('Promise'))
  .then(() => {
    process.nextTick(() => console.log('nested nextTick'));
  });
process.nextTick(() => console.log('nextTick'));

console.log('script end');

結果

Promiseの実行後にnextTickがキューに詰め込まれるので、実行も後になります。

script start
script end
nextTick
Promise
nested nextTick

両方のケース

基本

先程の2つの基本形を混ぜました

const fs = require('fs');
const path = require('path');

console.log('script start');

setImmediate(() => console.log('setImmediate'));
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs'));
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

console.log('script end');

結果

script start
script end
nextTick
Promise
setTimeout
fs
setImmediate

応用1

Promiseの中でnextTickを呼んだ場合どうなるでしょう

const fs = require('fs');
const path = require('path');

console.log('script start');

setImmediate(() => console.log('setImmediate'));
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs'));
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
  .then(() => console.log('Promise'))
  .then(() => {
    process.nextTick(() => console.log('nested nextTick'));
  });
process.nextTick(() => console.log('nextTick'));

console.log('script end');

結果

Promise実行中にnextTick queueが増えたので、まだMicrotasksのキューが空になりません。
なのでnested nextTickが実行されてからTimerが実行されます。

script start
script end
nextTick
Promise
nested nextTick
setTimeout
fs
setImmediate

応用2

setTimeoutの中でMicrotasks系の非同期関数を呼んだ場合どうなるでしょう

const fs = require('fs');
const path = require('path');

console.log('script start');

setImmediate(() => console.log('setImmediate'));
fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs'));
setTimeout(() => {
  console.log('setTimeout1')
  Promise.resolve().then(() => console.log('nested Promise'));
  process.nextTick(() => console.log('nested nextTick'));
}, 0);
setTimeout(() => console.log('setTimeout2'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

console.log('script end');

結果

setTimeoutの後でnested nextTickが実行されています。
これはEvent Loopの各キューの合間にMicrotasksが空になるまで実行されるためです。
注意としてTimerはキューが無くなるまではMicrotasksより先に実行されます

script start
script end
nextTick
Promise
setTimeout1
setTimeout2
nested nextTick
nested Promise
fs
setImmediate

応用

上記を理解していると、次のコードも理解できます。

gist.github.com

ref: Promises, Next-Ticks and Immediates— NodeJS Event Loop Part 3

結果

next tick1
next tick2
next tick3
promise1 resolved
promise2 resolved
promise3 resolved
promise4 resolved
promise5 resolved
next tick inside promise resolve handler
set timeout
set immediate1
set immediate2
set immediate3
set immediate4

まとめ

非同期関数の実行順についてまとめました。

ソース