翻译自:
https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4
![902ee1b82c7cfb665bfee5b342026aab.png](https://i-blog.csdnimg.cn/blog_migrate/424f77cafc962856333ddbc914192798.jpeg)
总览
我相信读者作为一个不管是初级中级还是高级的Node.js开发者,肯定对这些诸如事件循环、单线程、“setTimeout“、“setImmediate”如何运行等Node.js核心知识手到擒来。
首先,你知道Node.js使用不阻塞IO模型及异步编程风格,确实已经有无数的知名技术专家对文章或者博客介绍了这块,不过我敢保证其中很多或多或少有错误或者误导,从而引起误解,但这些文章又出现在百度首页上。更让人难过的是,他们让你以为自己得到了正确的知识。
所以到底什么是事件循环?Node.js到底是单线程还是多线程?
![1c353c6ec16a503471cacc19fedcf812.png](https://i-blog.csdnimg.cn/blog_migrate/2b6b879e1409fadaa924cd034be85eb7.jpeg)
事实上,当我在网上看到或者工作上遇到那些错误的不明确的在这个问题上的回答,着实让我大吃一惊。甚至我有一次因为我的回答没有符合面试官的期望而没有通过面试,当然他确定自己精通于此。
所以这片文章的主旨是来阐明你对Node.js核心的概念,它是怎么实现的,以及是怎么运作的。Node.js不只是“服务器上的Javascript“,有超过30%的代码是C++实现,而不是JS!接下来我们来探索一下C++的部分是如何运作的。
Node.js是单线程么?
- 是!答对了
- 不是!也答对了
![f2113e120c7bee5681a003a015c6b5d6.png](https://i-blog.csdnimg.cn/blog_migrate/9c56f61a5511d4a117ad1bb7663a2ff1.jpeg)
而且很多人会表达诸如多任务、单线程、多线程、线程池、epoll循环、事件循环等等。
让我们开始从头深入看看到底Node.js核心是怎么做的。
- 多任务就是一个处理器在同一时间可以做一个或多个任务(程序),然后并行处理
![9d221d6ebf27490dedaa89db347c368d.png](https://i-blog.csdnimg.cn/blog_migrate/927afc3e431e0d7d7926790bcf1d6edc.jpeg)
当现在有一个单核处理器,然后处理器一次只能处理一个任务,应用在任务结束之后发出通知,然后处理器去开始处理下一个任务,这就像Javascript语言中的generator函数一样,如果没有下一个了,它会返回当前的任务。在不远的过去,计算机在运行一个简单的应用或者游戏时因为无法调用通知,会变成无法访问,因为应用本身已经变成无法访问。
- 处理器是顶级处理容器,他自己有自己的专用内存系统。
这也就意味着在这单个处理器中,我们无法得到其他处理器内存的数据,来让两个处理器交换数据。我们必须额外做一些工作叫做进程间通讯(IPC),它通过系统sockets来运作。
https://en.wikipedia.org/wiki/Inter-process_communicationen.wikipedia.org在Unix下的工作都是围绕这sockets,Socket是一个整数数字,返回的时候Socket()系统的回调,叫做套接描述符或者文件描述符
![039b4ae59ae774d03a524a6ca7c1b3b2.png](https://i-blog.csdnimg.cn/blog_migrate/c78c76f6395c97c5f787a1f87e0573ae.png)
Sockets用虚拟的“interface”(read/write/pool/close/etc)来指向内核中的一些对象。系统套接字像TCP套接字一样工作:它们把数组转换成buffer再发送。所以当我们使用Javascript在两个进程中通讯的谁会,我们必须使用很多次JSON.stringify,尽管我们知道这个很慢,不过等一下,我们有线程!
![c4140f0bb64cfd880ea371931298aa80.png](https://i-blog.csdnimg.cn/blog_migrate/5dff81f25f042a3160c7b7077bb83702.png)
让我们看看我们如何让两个线程通讯
- 线程的执行是调度器独立管理的已编程指令最小序列。
线程跑在处理器里,一个处理器可以有很多线程,而且如果它们在同一个进程中,那它们共享一个内存。
酷!!!
也就是意味着如果我们想让两个线程通讯,我们怒需要做任何事情,如果我们在一个线程中定义了一个全局变量,我们可以在另外一个线程中直接取到它(它们都在同一个内存中,所以是真的很有效)
不过想象一下如果我们有一个函数在一个线程中,定义了一个变量叫“foo”,另外一个线程读取了它,那么问题是会发什么什么?
![50bb112e239b4b33acdb503266053e15.png](https://i-blog.csdnimg.cn/blog_migrate/bcfe053246bed4202cdb8880007e8890.png)
实际上,我们并不知道。可能第一个线程已经在另外一个线程读取之前就写入到内存中了,或者可能没有。
所以我们可以在第一个函数中取到变量值或者可能不能。
所以在多线程中编写代码会有一点困难,让我们看看Node.js在这方面怎么说。
Node.js说:我有单线程...
![037970c27020d58e7523d2fb38e585e4.png](https://i-blog.csdnimg.cn/blog_migrate/185af4332d15a6bac1ab395cfc814fd2.png)
实际上,Node.js有V8加持,代码运行在有eventloop的主线程中(我们称为单线程)。
不过众所周知,Node.js不只是V8,它还有很多接口是C++的,而且所有Eventloop管理的事情都是通过C++的Libuv来实现。
C++在Javascript代码后台运行,它可以访问线程。如果你使用Node.js来调用Javascript同步方法,它将一直在主线程中运行但是如果您运行一些异步的东西,它不会总是在主线程中运行:根据您使用的方法,事件循环可以将它路由到一个api,并且可以在另一个线程中处理它。
让我们看一个加密示例。它有许多CPU密集型的方法;有些是同步的,有些是异步的。让我们使用pbkdf2()方法。如果我们在2核处理器中运行同步版本并进行4次调用,如果一次调用的执行时间是2ms,那么所有4次调用的执行时间都是4*pbkdf2()执行时间(8ms)。
但是,如果我们在同一个CPU中运行这个方法的异步版本,执行时间将是2*pbkdf2()执行时间,因为处理器将采用默认的4个线程(您将理解下面的原因和方式),将其托管在两个进程中,并处理其中的pbkdf2()。
如果给node.js一个机会,它会为您并行运行。“所以使用异步方法”!!!
Node.js使用一组预先分配的线程,称为线程池,如果我们不指定要打开多少个线程,默认情况下它将打开4个线程。我们可以通过设置
uv_threadpool_size=110&&node index.js
或
process.env.uv_threadpool_size=62
。
所以node.js是多线程的吗?
- 注意!!!Node.js是在多线程下运行!是的!它是多线程
所以当人问你Node是多线程还是单线程时,你必须问一个问题:“啥时候”?
让我们看看TCP连接。
每个线程的连接
创建TCP服务器的简单方法是创建一个套接字,将这个套接字绑定到一个端口并在其上调用“listen”。
int server = socket();
bind(server, 8080);
listen(server);
在我们调用“listen”之前,这个套接字可以用于建立连接或接受连接。当我们叫“听”的时候,我们已经准备好接受连接了。
while(int conn = accept(server)) {
pthread_create(echo, conn)
}
void echo(int conn) {
char buf(4096);
while(int size = read(conn, buffer, sizeof buf)) {
write(conn, buffer,size);
}
}
当一个连接到达时,我们需要写入它,此时直到我们完成写入之前我们不能接受另一个连接,这就是为什么我们将它推入另一个线程。所以我们将套接字描述符和函数指针传递给线程。
现在,系统可以轻松地处理几千个线程,但在这种情况下,我们必须为每个连接向线程发送大量数据,而且它不能很好地扩展到20000到40000个并发连接。但让我们想想这个问题…
我们实际上只需要一个套接字描述符,并记住我们必须如何处理它。所以有一个更好的方法:我们可以使用epoll(unix)、kqueue(bsd)。
Epoll循环
让我们关注一下Epoll能给我们什么,用途是什么。使用Epoll,我们可以告诉内核我们感兴趣的事件,内核可以告诉我们何时发生我们要求的事情。在我们的例子中,它是一个传入的TCP连接。因此,我们创建一个Epoll描述符并将其添加到Epoll循环中,对其调用“wait”。当有一个传入的TCP连接时,它会被唤醒,然后我们将其添加到Epoll循环中并等待来自它的数据,以此类推。
这就是Event loop为我们做的事情!
举个例子:
当我们在同一个2核处理器上通过请求(http)下载一些东西时,4、6甚至8个请求都需要同样的时间。那是什么意思?这意味着这些限制与线程池中的不同。
这是因为操作系统负责下载;我们只要求它下载,然后问他:完成了吗?不?完成了吗?(监听Epoll中的“data”事件)。
APIs
那么哪个api对哪个功能有响应呢?
fs.*中的所有内容都使用uv线程池(除非它们是同步的)。阻塞调用是由线程进行的,完成后,会返回到事件循环。我们不能直接在epoll中“等待”,但我们可以用管道输送。管道有两个端点:一个是线程,完成后,它在管道中写入数据,另一个端点在Epoll循环中等待,当它获取数据时,Epoll循环将唤醒。所以Epoll对管道有响应。
主要功能和相应的api如下:
epool、kqueue、async,及不同系统上的其他接口
- TCP/UDP Servers and clients
- pipes
- dns.resolve
NGINX
- nginx signals ( sigterm )
- Child processes ( exec, spawn)
- TTY input ( console )
线程池
- fs.
- dns.lookup
事件循环负责发送和接收结果,所以它是一种中央调度,它将请求路由到C++ API,并像导演一样返回到JavaScript。
Event loop
那么什么是事件循环?它是一个无限的while循环,调用Epoll(Kqueue)“等待”或“池”,当Node.js发生有趣的事情(回调、事件、fs)时,它会路由到Node.js,并且在Epoll没有等待的情况下退出。这就是Node.js中异步工作的方式,也是我们称之为事件驱动的原因。事件循环允许Node.js执行非阻塞I/O操作。尽管JavaScript是一个线程,通过将操作卸载到系统内核,只要是可能的,它是一个无限循环,调用Epoll(Kqueue)“等待”或“池”,当Node.js发生有趣的事情(回调、事件、fs)时,它会路由到Node.js,并且在Epoll没有什么等待的时候退出。这就是node.js中异步工作的方式,也是我们称之为事件驱动的原因。事件循环允许node.js执行非阻塞I/O操作。尽管Javascript是单线程的,就可以将操作尽可能转移到系统内核上。
Node.js事件循环的一次迭代称为Tick,并且自身具有阶段。
Node.js官方文档中有关事件循环阶段、计时器和process.nextTick()的更多详细信息可以访问(如果你需要阅读一下):https://nodejs.org/es/docs/guides/event-loop-timers-and-nexttick/
自从Node.js v10.5.0发布以来,有一个新的worker_threads
模块可用。
这个worker_threads模块允许使用并行执行Javascript的线程。
如何使用:
Workers(线程)对于执行CPU密集型Javascrip的操作非常有用。它们对I/O密集型工作没有多大帮助。Javascript内置的异步I/O操作比Workers更高效。
与子进程或群集不同,工作线程可以共享内存。它们通过传输ArrayBuffer实例或共享ShareDarrayBuffer实例来实现。
Node.js官方文档中有关工作线程的更多详细信息:https://nodejs.org/api/worker_threads.html
恭喜你阅读完了整篇文! 你太厉害了。
❤ 感谢阅读,如果这篇文章有帮助,请点击hit the clapp!别忘了看看我的其他文章,下一篇是关于MongoDB分片的。祝你好运!