[2014.10.08更新] 本文内容有错误,请参考新版本。
Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制。
JavaScript语言就采用这种机制,来解决单线程运行带来的一些问题。
本文参考C. Aaron Cois的《Understanding The Node.js Event Loop》,解释什么是Event Loop,以及它与JavaScript语言的单线程模型有何关系。
想要理解Event Loop,就要从程序的运行模式讲起。运行以后的程序叫做"进程"(process),一般情况下,一个进程一次只能执行一个任务。
如果有很多任务需要执行,不外乎三种解决方法。
(1)排队。因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。
(2)新建进程。使用fork命令,为每个任务新建一个进程。
(3)新建线程。因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。(进程和线程的详细解释,请看这里。)
以JavaScript语言为例,它是一种单线程语言,所有任务都在一个线程上完成,即采用上面的第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。
你也许会问,JavaScript为什么是单线程,难道不能实现为多线程吗?
这跟历史有关系。JavaScript从诞生起就是单线程。原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。(Worker API可以实现多线程,但是JavaScript本身始终是单线程的。)
如果某个任务很耗时,比如涉及很多I/O(输入/输出)操作,那么线程的运行大概是下面的样子。
上图的绿色部分是程序的运行时间,红色部分是等待时间。可以看到,由于I/O操作很慢,所以这个线程的大部分运行时间都在空等I/O操作的返回结果。这种运行方式称为"同步模式"(synchronous I/O)或"堵塞模式"(blocking I/O)。
如果采用多线程,同时运行多个任务,那很可能就是下面这样。
上图表明,多线程不仅占用多倍的系统资源,也闲置多倍的资源,这显然不合理。
Event Loop就是为了解决这个问题而提出的。Wikipedia这样定义:
"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"
简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。
上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,所以不存在红色的等待时间。等到I/O程序完成操作,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。
可以看到,由于多出了橙色的空闲时间,所以主线程得以运行更多的任务,这就提高了效率。这种运行方式称为"异步模式"(asynchronous I/O)或"非堵塞模式"(non-blocking mode)。
这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果部署得好,JavaScript程序是不会出现堵塞的,这就是为什么node.js平台可以用很少的资源,应付大流量访问的原因。
(完)
foo 说:
这不是nodejs特有的机制,几乎所有语言都可以实现这个。所以“但也因此使它具备了其他语言不具备的优势。”,是错误的
2013年10月21日 10:48 | # | 引用
linbo 说:
异步和非阻塞是不同的概念
2013年10月21日 10:53 | # | 引用
zhong 说:
我觉得可以这么说,JS默认使用这个机制,其它语言虽然支持而非默认。
比如Java需要网络访问,谁不是开一个新线程。
2013年10月21日 11:07 | # | 引用
炎燎 说:
确实是,同步和异步,阻塞和非阻塞,是两组不同的概念,两个之间没有必然的联系。
JavaScript在处理回调时也存在阻塞现象
2013年10月21日 11:09 | # | 引用
D瓜哥 说:
这两天正在关注着方面的问题。正好解答了我的疑问!顶阮老师!哈哈
2013年10月21日 11:34 | # | 引用
shawn 说:
event loop只使用了一个线程,如果同时有多个并发的IO请求,那么这些请求也将在event loop线程中排队等待罗?
2013年10月21日 11:34 | # | 引用
woosley 说:
所以要用非阻塞
2013年10月21日 11:41 | # | 引用
muzuiget 说:
Python 可以用 gevent 来实现。
2013年10月21日 12:49 | # | 引用
电玩 说:
大哥这文章是给初级程序员看的吧?
2013年10月21日 12:55 | # | 引用
学良 说:
阻塞有阻塞的好处,非阻塞有非阻塞的优势,有些非阻塞的消息,也需要主线程通过自旋来提取,其实耗费的也是大量的CPU。根据架构不同,生产场景不同,选择也不同。详见UNIX系统的IO模型
2013年10月21日 13:03 | # | 引用
shawn 说:
非阻塞的话就意味着多个线程,那这么我理解成用I/O完成端口,如果这样的话,就直接在主线程上好了,也是不阻塞,那么就不需要这个event loop线程了?
2013年10月21日 13:16 | # | 引用
依云 说:
Python 的 Tornado 框架就只一个线程。
所以 Node.js 用起来很别扭,各种回调。同步操作最重复的一点就是方便,这也是为什么大部分语言默认使用同步方式。这也是为什么现在流行协程的原因。
另外,同步访问不意味着必须开一个新进程或者进程,编程语言底层可以使用事件 I/O 来处理。比如 Haskell 的 I/O 线程和 Erlang 的进程。
2013年10月21日 17:17 | # | 引用
ffflow 说:
同步IO != 阻塞IO
2013年10月21日 19:58 | # | 引用
dc 说:
如果在前一个回调执行完之前,后一个回调来了,也就是说,event loop的两个回调在时间上有重叠的部分会怎么样?
2013年10月21日 21:15 | # | 引用
run_mei 说:
感觉这里大多的做前端的,对后台没有什么感觉。其实 javascript 语言与 event loop 一点关系都没有。javascript 才没有用 event loop 呢? javascript 的一个库node.js 采用了 event loop。但不能说 javascript 采用了 event loop。
2013年10月21日 22:04 | # | 引用
尹良灿 说:
"简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";..."
"每当遇到I/O的时候,主进程就让Event Loop进程去通知相应的I/O程序"
这里是不是有矛盾?是线程还是进程?
2013年10月23日 12:38 | # | 引用
阮一峰 说:
@尹良灿:
谢谢指出,我太疏忽了,已经改过来了。
2013年10月23日 21:00 | # | 引用
eyny 说:
深入浅出,浅显易懂啊!!使用dojo的人也觉得很有用,受益匪浅!
2013年10月24日 13:13 | # | 引用
Onyx 说:
这么说Event Loop内部也是通过异步方式跟IO打交道,不然也会有红色等待阶段,而Event Loop依赖Worker API来实现的异步?
2013年10月25日 15:58 | # | 引用
yougen 说:
"每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行..."
主线程去通知Event loop线程的时候,是不是依旧存在短暂的阻塞,把控制权交给了Event Loop?只是这个过程很短暂,可以忽略?
2013年10月27日 16:44 | # | 引用
Ricky 说:
大神,图挂了呀。
2013年10月30日 13:03 | # | 引用
asdf 说:
是阻塞吧?
2013年11月 3日 21:17 | # | 引用
王川 说:
这跟语言没关系,应该是系统提供的功能,比如unix的select,poll,linux下的epoll,BSD下的kqueue等等
2013年11月 4日 12:49 | # | 引用
老許 说:
不是說js是單線程嗎?怎麼會有主線程又有event loop線程,不是矛盾嗎?
2013年11月 5日 20:45 | # | 引用
GrahamLe 说:
阮老师,你好,我想请教一下,你最新写的那本书“JavaScript 标准参考教程(alpha)”这个是在github上用github page搭的,是项目page,我想请问下能否有相关的这样的教程,我尝试过很多次,都没成功。多谢~~
2013年11月14日 19:35 | # | 引用
Hux 说:
我也想问这个……
2013年11月18日 17:06 | # | 引用
烤全羊 说:
说了这么长,还是多线程解决的问题
2013年12月 1日 20:32 | # | 引用
guoguofish 说:
我的理解是Nodejs 你写的主代码是起点,结束后进入事件循环,这时候有个叫事件队列的数据结构里已经存在了一些需要处理的任务,这些任务可能还会发新的任务。这个事件循环怎么理解,我想象的就像一个人每天都要做同样的工作,天天如此,这就是循环。每天的工作就是先收集事件,然后处理事件(或者先处理后收集,谁知道)然后下一天.....
Nodejs里有process.nextTick()函数,tick的意思是滴答声,暗含的意思就是秒针走了一秒,每一个tick里,nodejs都可以做一堆的事,比如 收事件 处理事件,如果发现再也没有新的事件到来,那就退出.
2013年12月13日 10:31 | # | 引用
哎呦喂 说:
在其他地方见过这篇文章,今天在这看到原创了,又细读了一次,觉着这种设计理念真是不错。
2013年12月19日 17:10 | # | 引用
走着 说:
LOOP里如果有多个任务一样还会阻塞吧
2014年5月 9日 09:55 | # | 引用
不明白 说:
我理解多线程,但是我没有看明白你的文章
我理解着你应该说的是 异步和回调
2014年5月16日 15:02 | # | 引用
不明白 说:
Event Loop线程再把结果返回主线程.
此句说了2个线程,我想"Event Loop线程"应该是你说的单线程.而主线程应该为另一个线程--浏览器线程
2014年5月16日 15:04 | # | 引用
Emma 说:
异步有异步的好处,但是如果后面的代码需要用到前面回调函数里得出的值时我们该怎样去处理呢,特别是在处理数据库的时候,数据库处理占用时间很大,但又必须的获得前面的数据。
2014年5月22日 17:28 | # | 引用
yeye 说:
wowwow
喜欢你的博客
2014年5月28日 20:26 | # | 引用
nebula 说:
你去看看Promise规范,它可以用同步的习惯进行编程。
2014年6月 9日 11:33 | # | 引用
rainypin 说:
如果主线程一直没有结束,那么事件循环中的回调函数是不是一直没有机会得以执行呢?
2014年6月10日 11:17 | # | 引用
寻觅 说:
是这样的。event loop只是让线程能一直处理该线程本身应该处理的东西,不会因为需要非本线程的资源而空闲。但如果线程本身就要处理多个运算的话,上一个运算没有结束,下一个运算是不会进行的。
2014年8月 4日 15:00 | # | 引用
李博 说:
node.js底层本身应该也是多线程的
2014年8月19日 17:00 | # | 引用
李博 说:
顶!
2014年8月20日 12:34 | # | 引用
李博 说:
请举例说明?
2014年8月20日 12:39 | # | 引用
1024bit 说:
阮一峰的文章最大的有点在于其科普性和简洁性,我想这是阮一峰的真正目的。
最近看到很多抱怨文章深度不够,我想说,够深度的内容都在草案里,你们可以去找相关规范草案进行深度挖掘。
因此,阮一峰的博文就是一个字典。
2015年4月20日 12:17 | # | 引用
poic 说:
这个机制是从浏览器来的,为什么浏览器用这个机制?因为浏览器是一个GUI程序。
————几乎所有的GUI程序和GUI框架都是这个机制。从有图形界面开始,就有这个机制(说不定还更早)。
所以这跟语言没什么关系,也当然不能算是node独有的优势。
2017年3月 1日 20:03 | # | 引用
poic 说:
如果有兴趣追究“为什么GUI就要用单线程事件驱动的机制”,很有趣,网上也有不少文章可以搜。
简而言之,GUI系统中你如果想用多线程更新,细粒度锁保证线程安全的话,最终总是特别容易陷入死锁,因为总会有从两个相反方向来的调用试图访问一些绘图相关的共享数据。
最终人们不约而同发现了同一个简单粗暴的解决方案: 不用什么细粒度锁,整个绘图相关的部分用同一把大锁;于此等同的方式就是:整个绘图部分用一个单独的线程,即所谓“单线程拘禁”,这在功能上与一把大锁是相等的。
这个线程就是 Event Dispatch 线程,又叫UI线程,被node拿来做io线程。
2017年3月 1日 20:12 | # | 引用
阿集 说:
IO请求的线程是浏览器的线程池去处理的,等结果放回,才会被放进event loop线程里面
2017年8月24日 12:00 | # | 引用
sappho 说:
看完再回头看,竟然说这篇文章说的都是错的。。。错了就删掉啊哥
2018年3月 5日 10:13 | # | 引用
pp 说:
建議開頭這行顯眼一點,一開始沒注意,以為是廣告區塊,看到 sappho 的留言才發現這行。
"[2014.10.08更新] 本文内容有错误,请参考新版本。"
真心建議,確定的才寫,我自己寫,沒確定的都不敢寫。
畢竟可能讓很多人學到錯的東西。
2018年3月25日 20:51 | # | 引用
xw 说:
阮老师 请问一下 那这个消息线程 是在什么时候 执行的呢
2018年10月12日 18:59 | # | 引用
nelhu 说:
现在看这篇文章,过时吗
2019年6月11日 14:56 | # | 引用
阿郑 说:
我也同样存在这个疑问?前提是我相信nodejs真的是单线程(只有一个主线程在工作)?难道所谓的协程就是用来处理这些异步任务的吗?
不知道您现在解决这个疑问了没,如果解决了,可以帮我解答一下吗
邮箱:[email protected]
2019年10月24日 11:36 | # | 引用
机车虾前端 说:
你可以把它想象成异步回调过程,多个并发,就是触发多个异步过程,只要有反馈结果就会通知主线程我有结果了,下次执行的事后把我也带上处理,
2020年5月27日 15:03 | # | 引用
再见陛下 说:
没看明白红色和橙色(等待时间和空闲时间)有啥区别?
2020年9月23日 09:23 | # | 引用
小前端 说:
橙色可以在执行其他请求,发送其他I/O命令给Eventloop,但是红色就什么都不能做
2020年10月 7日 15:32 | # | 引用
往往会 说:
C++ 没有所谓的事件循环
2020年11月13日 16:05 | # | 引用
samkr 说:
主线程是共用一个线程,这个就涉及宏任务,微任务吧,会推入堆栈,按顺序执行
2021年4月 7日 16:44 | # | 引用
hxx 说:
是这样的,所以才会有setTimeout设置为0时并不是立即执行里面的代码,而是要等主线程执行完当前任务之后才去立即执行setTimeout的回调
2022年9月 5日 14:35 | # | 引用
li 说:
之前看的感觉巨难,怎么这里讲完这么简单?
2022年9月 5日 21:01 | # | 引用