-
Notifications
You must be signed in to change notification settings - Fork 211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Node.js的event loop及timer/setImmediate/nextTick #26
Comments
@creeperyang Hi,我想说的是那个105ms的例子,我用js在浏览器当中执行异步代码的原理也能解释的通,那么问题是不是WebAPIs是浏览器中的,在Nodejs中就是libuv,EventLoop只是等待堆栈执行完才把TaskQueue的回调Push到堆栈执行,按照我的理解是,someAsyncOperation方法先完成了,他被push到任务队列中,然后EventLoop判断Stack中空了,执行那剩下10秒,但是过了5s后,Settimeout回调任务完成了,被Push到任务队列中,但是当前Stack正在执行后面那5s不为空,所以EventLoop要等到为空才执行这个回调,所以是105ms。 js在web中的例子:http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D |
@zColdWater 你理解得应该没什么问题,就是表述有点不是很好懂(比如没有event loop为空的说法)。 下面总结几点关于event loop的:
|
那在H5新特征的多线程JS处理代码中,这个异步该如何处理呢? |
@creeperyang 谢谢,🍻。 |
@chn777 你是指web worker吗?web worker工作在单独的线程,有自己的event loop。 |
@creeperyang 是的是的,谢谢了 |
Hi, 不是很理解poll阶段具体是负责什么业务。 |
|
您好,现在才看到您这篇文章,想请问以下代码中:
执行结果:
我在setImmediate中又调用了一个setImmediate,称外层setImmediate为A,内层setImmediate为B,那么我想问,为什么此时B不会紧接着A执行,而是使得其中的nextTick先执行呢?我认为是B应该也被加入了setImmediate所在的队列,应该会继续执行啊,希望能得到您的回复,谢谢! |
@Julyrainy 简单说, 可以联系 #21 一起看。 |
@creeperyang 多谢您的回复,主要就是macrotask和microtask我不理解。
我的困惑就在于此,在我上方例子中,A执行的时候必然是处于check阶段,A执行时候新注册了一个setImmediate,也就是B,此时B为何不能继续执行?是因为A执行之后直接退出了check阶段吗? 另外看过好多资料说macrotask一次tick只能执行一个,macrotask队列中的下一个task得等到下次tick才能执行,而microtask能执行多次,这是正确的说法吗? |
@Julyrainy https://nodejs.org/api/timers.html#timers_setimmediate_callback_args
文档中很清楚地写明了:
其实这很好理解,一个 event loop 有多个阶段,每个阶段做对应的事,timer 阶段会把需要执行的 回调 (队列)都执行,check 阶段也会把 把需要执行的 回调 (队列)都执行。 |
一个帮助理解的例子: const async_hooks = require('async_hooks')
const fs = require('fs')
let indent = 0
async_hooks.createHook({
init(asyncId, type, triggerId) {
const cId = async_hooks.currentId()
print(`${getIndent(indent)}${type}(${asyncId}): trigger: ${triggerId} scope: ${cId}`)
},
before(asyncId) {
print(`${getIndent(indent)}before: ${asyncId}`)
indent += 2
},
after(asyncId) {
indent -= 2
print(`${getIndent(indent)}after: ${asyncId}`)
},
destroy(asyncId) {
print(`${getIndent(indent)}destroy: ${asyncId}`)
},
}).enable()
function print(str) {
fs.writeSync(1, str + '\n');
}
function getIndent(n) {
return ' '.repeat(n)
}
print('start')
setTimeout(() => {
print('--outter: timeout1')
setImmediate(() => {
print('--inner: setImmediate1')
process.nextTick(() => {
print('--inner-inner: nextTick')
})
})
setTimeout(() => {
print('--inner: setTimeout')
})
setImmediate(() => {
print('--inner: setImmediate2')
})
process.nextTick(() => {
print('--inner: nextTick')
})
})
process.nextTick(() => {
print('--outter: nextTick')
})
setTimeout(() => {
print('--outter: timeout2')
})
print('end') 借助 async_hooks 模块,可以看到以下输出: //// loop 1
start
Timeout(2): trigger: 1 scope: 1
TIMERWRAP(3): trigger: 1 scope: 1
TickObject(4): trigger: 1 scope: 1
Timeout(5): trigger: 1 scope: 1
end
before: 4
--outter: nextTick
after: 4
///// loop 2
before: 3
before: 2
--outter: timeout1
Immediate(6): trigger: 2 scope: 2
Timeout(7): trigger: 2 scope: 2
Immediate(8): trigger: 2 scope: 2
TickObject(9): trigger: 2 scope: 2
after: 2
before: 5
--outter: timeout2
after: 5
after: 3
before: 9
--inner: nextTick
after: 9
destroy: 4
destroy: 2
destroy: 5
destroy: 9
///// loop 3
before: 3
before: 7
--inner: setTimeout
after: 7
after: 3
destroy: 7
before: 6
--inner: setImmediate1
TickObject(10): trigger: 6 scope: 6
after: 6
before: 8
--inner: setImmediate2
after: 8
before: 10
--inner-inner: nextTick
after: 10
destroy: 6
destroy: 8
destroy: 10
destroy: 3 很容易看出,两个 timeout 回调在同一个 loop 执行,两个 setImmediate 同一个 loop 执行。 当我们去除 async_hooks 模块,重复跑这段代码,有时可能会有不同的输出: start
end
--outter: nextTick
--outter: timeout1
--inner: nextTick
--outter: timeout2
--inner: setTimeout
--inner: setImmediate1
--inner: setImmediate2
--inner-inner: nextTick 或: start
end
--outter: nextTick
--outter: timeout1
--outter: timeout2
--inner: nextTick
--inner: setImmediate1
--inner: setImmediate2
--inner-inner: nextTick
--inner: setTimeout 我们知道 之所以出现这样的情况,应该跟 timer 的时间判断有关。 |
@creeperyang 万分感谢您细心的回复,我有点清晰了,我在好好研读一下 |
@Julyrainy 一次事件循环只执行一次宏任务(task), 然后执行多个微任务(microtask). 这个是浏览器端的事件循环模型, 不适用于 nodejs. 我之前也在关于 nodejs 的事件循环看到这种说法, 这个是错误的. 关于浏览器端的事件循环可以参考 https://html.spec.whatwg.org/multipage/webappapis.html#event-loops . 如果楼主有兴趣的话, 不妨也翻译下. |
@creeperyang 我看到nextTick的这种用法:
请教一下,这里的nextTick嵌套,是不是在每个“阶段”执行一次?否则岂不是死循环了?
说的是不是这种情况? |
@mygaochunming 是。但不是每个“阶段”执行一次,而是在该阶段持续执行 nextTick 注册的回调。 |
@creeperyang 我上面那段代码摘自http://www.cnblogs.com/lengyuhong/archive/2013/03/31/2987745.html 这里岂不是死循环,即文章中说的“假死”。 |
@mygaochunming 你贴的代码的确是死循环, |
macrotasks: setTimeout ,setInterval, setImmediate,requestAnimationFrame,I/O ,UI渲染 当一个程序有:setTimeout, setInterval ,setImmediate, I/O, UI渲染,Promise ,process.nextTick, Object.observe, MutationObserver的时候: 1.先执行 macrotasks:I/O -》 UI渲染-》requestAnimationFrame 2.再执行 microtasks :process.nextTick -》 Promise -》MutationObserver ->Object.observe 3.再把setTimeout setInterval setImmediate【三个货不讨喜】 塞入一个新的macrotasks,依次:setTimeout ,setInterval --》setImmediate
|
输出
能解释一下是为什么么? |
Hi @Zerxoi 感謝你提供了這麼棒的一個範例,這個範例可以幫助我們更了解 libuv 的 event loop 我在 libuv event loop 代碼插入了一堆 printf 觀察了一下,大概至上是改這樣: int count = 0;
while (r != 0 && loop->stop_flag == 0) {
count++;
printf("\n\n===== LOOP ROUND %d =====\n", count);
uv__update_time(loop);
printf("[uv__run_timers]: enter\n");
uv__run_timers(loop);
printf("[uv__run_timers]: exit\n");
printf("[uv__run_pending]: enter\n");
ran_pending = uv__run_pending(loop);
printf("[uv__run_pending]: exit\n");
printf("[uv__run_idle]: enter\n");
uv__run_idle(loop);
printf("[uv__run_idle]: exit\n");
printf("[uv__run_prepare]: enter\n");
uv__run_prepare(loop);
printf("[uv__run_prepare]: exit\n");
... 此外,因為 uv__io_poll (kqueue.c) 的實作也很長,我在裡面一些 if else 的分支部分也插入 printf ,像是這樣 if (ev->filter == EVFILT_READ) {
printf("[uv__io_poll]: ev->filter == EVFILT_READ\n");
...
}
if (ev->filter == EV_OOBAND) {
printf("[uv__io_poll]: ev->filter == EV_OOBAND\n");
...
}
if (ev->filter == EVFILT_WRITE) {
printf("[uv__io_poll]: ev->filter == EVFILT_WRITE\n");
...
} 然後編譯 node,執行你給的範例 const fs = require('fs')
const now = Date.now();
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
} 得到的結果是這樣子
若將 readFile 拿掉: const fs = require("fs");
const now = Date.now();
setTimeout(() => console.log("timer"), 10);
setImmediate(() => console.log("immediate"));
while (Date.now() - now < 1000) {} 則會得到
可以確定的是 readFile 的 callback 確實是在 uv__io_poll 這個階段執行的,不是 uv__run_pending |
@rueian 👍 提供的这段 log 对了解 libuv 的处理流程很有帮助! |
据说 requestAnimationFrame 不算是 macro task。 https://stackoverflow.com/questions/43050448/when-will-requestanimationframe-be-executed |
Two questions
So it seems there is only one thread in Node.js, do I understand right? 2 文中提到 ‘I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。’ 。 但是原文中的意思是這個階段只是執行系統圖回調,你所説的執行所有回調是什麽意思呢?能都詳細説明,謝謝。 |
@LeonAppDev Q2: 我看了下https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/的最新文档似乎有更新,我从头再看一遍来给一个确切的回答。 关于Q2,最新的文档似乎跟我原来看的略有不同,根据最新的文档,有些措辞需要更新,我会更新上面的相关部分,并在这里重点讲一下 pending(I/O) callbacks phase vs poll phase :
大部分的I/O回调会在poll阶段被执行,但某些系统操作(比如TCP类型错误)执行回调会安排在pending callbacks阶段。 更多细节在http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop。 |
順帶一提 Node.js 10 開始有可以利用多線程的 Worker Thread 功能 |
@creeperyang 非常感謝及時回复,那么关于Q2我还有一個问题,为什么系统操作的回調不会在poll阶段來执行呢?我的理解是Poll阶段只执行用户定义和Node库定义的回調,也就是比較高層的回調,而把底層的回調放在Pending Callbacks階段執行,那麽這麽做有什麽好處呢?是不是爲了優先回復Success的用戶request,而把failure的response放到後面來執行從而提高用戶的體驗?這是我的理解。 |
@LeonAppDev 更准确的说, 详情可以参考Node.js更新相应文档的原因:
下面讲一讲 int uv__tcp_connect(uv_connect_t* req,
uv_tcp_t* handle,
const struct sockaddr* addr,
unsigned int addrlen,
uv_connect_cb cb) {
int err;
int r;
assert(handle->type == UV_TCP);
if (handle->connect_req != NULL)
return -EALREADY; /* FIXME(bnoordhuis) -EINVAL or maybe -EBUSY. */
err = maybe_new_socket(handle,
addr->sa_family,
UV_STREAM_READABLE | UV_STREAM_WRITABLE);
if (err)
return err;
handle->delayed_error = 0;
do {
errno = 0;
r = connect(uv__stream_fd(handle), addr, addrlen);
} while (r == -1 && errno == EINTR);
/* We not only check the return value, but also check the errno != 0.
* Because in rare cases connect() will return -1 but the errno
* is 0 (for example, on Android 4.3, OnePlus phone A0001_12_150227)
* and actually the tcp three-way handshake is completed.
*/
if (r == -1 && errno != 0) {
if (errno == EINPROGRESS)
; /* not an error */
else if (errno == ECONNREFUSED)
/* If we get a ECONNREFUSED wait until the next tick to report the
* error. Solaris wants to report immediately--other unixes want to
* wait.
*/
handle->delayed_error = -errno;
else
return -errno;
}
uv__req_init(handle->loop, req, UV_CONNECT);
req->cb = cb;
req->handle = (uv_stream_t*) handle;
QUEUE_INIT(&req->queue);
handle->connect_req = req;
uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);
if (handle->delayed_error)
uv__io_feed(handle->loop, &handle->io_watcher);
return 0;
} 从libuv源码可以看到,对 |
赞,关注一波 |
问题1: 看到有回复得出readFile的回调在 I/O callbacks 触发,按照这样的话,我觉得流程是这样:
但这样的话完全不符合实际结果,到底是什么原因? const fs = require('fs')
fs.readFile(__filename, () => {
console.log('readFile')
setTimeout(() => {
console.log('readFile timeout')
}, 0)
setImmediate(() => {
console.log('readFile immediate')
})
fs.readFile(__filename, () => {
console.log('readFile readFile')
})
})
setImmediate(() => {
console.log('immediate')
})
setTimeout(() => {
console.log('timeout')
}, 0) 结果是
问题2: setImmediate里面嵌套 setImmediate(() => {
console.log('setImmediate')
setTimeout(() => {
console.log('setImmediate 里面的 setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate 里面的 setImmediate')
})
});
// 结果
// setImmediate
// setImmediate 里面的 setTimeout
// setImmediate 里面的 setImmediate
// 或
// setImmediate
// setImmediate 里面的 setImmediate
// setImmediate 里面的 setTimeout setTimeout里面嵌套 setTimeout(() => {
console.log('setImmediate')
setTimeout(() => {
console.log('setImmediate 里面的 setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate 里面的 setImmediate')
})
}, 0);
// 结果
// setImmediate
// setImmediate 里面的 setImmediate
// setImmediate 里面的 setTimeout |
@surahe 刚好发现一篇文章, 解释的很清楚 https://segmentfault.com/a/1190000013102056#articleHeader9 |
本文是对Node.js官方文档The Node.js Event Loop, Timers, and
process.nextTick()
的翻译和理解。文章并不是一字一句严格对应原文,其中会夹杂其它相关资料,以及相应的理解和扩展。相关资料:
什么是事件循环(
Event loop
)?Event loop是什么?
WIKI定义:
Event loop是一种程序结构,是实现异步的一种机制。Event loop可以简单理解为:
所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后主线程继续执行后续的任务。
一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。
主线程不断重复上面的第三步。
对JavaScript而言,Javascript引擎/虚拟机(如V8)之外,JavaScript的运行环境(runtime,如浏览器,node)维护了任务队列,每当JS执行异步操作时,运行环境把异步任务放入任务队列。当执行引擎的线程执行完毕(空闲)时,运行环境就会把任务队列里的(执行完的)任务(的数据和回调函数)交给引擎继续执行,这个过程是一个不断循环的过程,称为事件循环。
注意:JavaScript(引擎)是单线程的,Event loop并不属于JavaScript本身,但JavaScript的运行环境是多线程/多进程的,运行环境实现了Event loop。
另外,视频What the heck is the event loop anyway 站在前端的角度,用动画的形式描述了上述过程,可以便于理解。
解释Node.js的Event loop
当Node.js启动时,它会初始化event loop,处理提供的代码(代码里可能会有异步API调用,timer,以及
process.nextTick()
),然后开始处理event loop。下面是node启动的部分相关代码:
Event Loop的执行顺序
下面的示意图展示了一个简化的event loop的操作顺序:
(图来自Node.js API)
图中每个“盒子”都是event loop执行的一个阶段(phase)。
每个阶段都有一个FIFO的回调队列(queue)要执行。而每个阶段有自己的特殊之处,简单说,就是当event loop进入某个阶段后,会执行该阶段特定的(任意)操作,然后才会执行这个阶段的队列里的回调。当队列被执行完,或者执行的回调数量达到上限后,event loop会进入下个阶段。
Phases Overview 阶段总览
setTimeout()
和setInterval()
设定的回调。close
回调,timer的回调,和setImmediate()
的回调。setImmediate()
设定的回调。socket.on('close', ...)
的回调。Phases in Detail 阶段详情
timers
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
注意:这个下限时间有个范围:
[1, 2147483647]
,如果设定的时间不在这个范围,将被设置为1。I/O callbacks
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到
ECONNREFUSED
,类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。
poll
poll 阶段有两个主要功能:
当event loop进入 poll 阶段,并且 没有设定的timers(there are no timers scheduled),会发生下面两件事之一:
如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
如果 poll 队列为空,则发生以下两件事之一:
setImmediate()
设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。setImmediate()
设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):
check
这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被
setImmediate()
设定的回调,event loop会转到 check 阶段而不是继续等待。setImmediate()
实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv
的API来设定在 poll 阶段结束后立即执行回调。
通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被
setImmediate()
设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。close callbacks
如果一个 socket 或 handle 被突然关掉(比如
socket.destroy()
),close事件将在这个阶段被触发,否则将通过process.nextTick()
触发。event loop的一个例子讲述
当event loop进入 poll 阶段,它有个空队列(
fs.readFile()
尚未结束)。所以它会等待剩下的毫秒,直到最近的timer的下限时间到了。当它等了95ms,
fs.readFile()
首先结束了,然后它的回调被加到 poll的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的
下限时间,然后回到 timers 阶段,执行timer的回调。
所以在示例里,回调被设定 和 回调执行间的间隔是105ms。
setImmediate()
vssetTimeout()
setImmediate()
和setTimeout()
是相似的,区别在于什么时候执行回调:setImmediate()
被设计在 poll 阶段结束后立即执行回调;setTimeout()
被设计在指定下限时间到达后执行回调。下面看一个例子:
代码的输出结果是:
是的,你没有看错,输出结果是 不确定 的!
从直觉上来说,
setImmediate()
的回调应该先执行,但为什么结果随机呢?再看一个例子:
结果是:
很好,
setImmediate
在这里永远先执行!所以,结论是:
setImmediate
的回调永远先执行。那么又是为什么呢?
看
int uv_run(uv_loop_t* loop, uv_run_mode mode)
源码(deps/uv/src/unix/core.c#332):上面的代码看起来很清晰,一一对应了我们的几个阶段。
setTimeout(fn, 0)
等价于setTimeout(fn, 1)
),那么setTimeout
的回调会首先执行。setImmediate
的回调会先执行。fs.readFile
回调里设置的,setImmediate
始终先执行?因为fs.readFile
的回调执行是在 poll 阶段,所以,接下来的 check 阶段会先执行setImmediate
的回调。UV_RUN_ONCE
模式下,event loop会在开始和结束都去执行timer。理解
process.nextTick()
直到现在,我们才开始解释
process.nextTick()
。因为从技术上来说,它并不是event loop的一部分。相反的,process.nextTick()
会把回调塞入nextTickQueue
,nextTickQueue
将在当前操作完成后处理,不管目前处于event loop的哪个阶段。看看我们最初给的示意图,
process.nextTick()
不管在任何时候调用,都会在所处的这个阶段最后,在event loop进入下个阶段前,处理完所有nextTickQueue
里的回调。process.nextTick()
vssetImmediate()
两者看起来也类似,区别如下:
process.nextTick()
立即在本阶段执行回调;setImmediate()
只能在 check 阶段执行回调。The text was updated successfully, but these errors were encountered: