Giter Site home page Giter Site logo

blogs's Introduction

Hi there 👋

  • I am Sean.
  • I am a Full-stack engineer working for an ecommerce company located in Shenzhen, China.
  • I believe coding can make the world a better place.
  • I believe bitcoin is the future money.₿₿₿
  • I love ethereum.
  • I don't like ethereum gas fee.

blogs's People

Contributors

xiaocongdong avatar

Watchers

 avatar

blogs's Issues

深入理解NodeJs事件循环机制

深入理解NodeJs事件循环机制

看完这篇文章,你可以回答以下问题:

  • 什么是NodeJs的事件循环?
  • NodeJs是单线程的吗?
  • Event Loop 的每个阶段都做了些什么?
  • setTimeout实现原理
  • setImmedate和process.nextTick的区别

一些误解

事件循环(event loop)独立于主线程(main thread)

我们经常在网上看到一些关于JavaScript事件循环的架构图,他们会把事件循环从主线程里面抽离出来。这样就会使我们容易产生以下的错觉:事件循环是由另外一个线程维持的,这个线程监听一个事件队列,当事件队列不为空时而且主执行栈没有被执行的代码时,事件循环就会把回调函数从事件队列里面拿出来放到主执行栈(main thread)里面执行。其实这种想法是错误的,对于nodejs,无论是关于事件循环的C代码,还是你自己写的JavaScript代码都会在同一个线程, 即是主线程里面执行。

Node是单线程的

这种说法其实是不全面的,只能说Node表现起来是单线程的,这样我们在编写Node代码时就不用考虑到多线程对共享数据进行操作时的安全问题。其实在Node里面某些异步操作,例如文件读写, dns解析等使用的是libuv的线程池(thread pool), 这些操作实际上和主线程(main thread)不是同一个线程,所以,NodeJs is not so single-thread.

一些和Event Loop 相关的C语言代码

要理解NodeJs异步操作实现的原理,要先看一些C代码

假如我们现在要实现一个tcp服务器,以下是这个服务的伪C代码

// 创建一个socket
int server = socket()
// 将这个socket和80端口绑定在一起
bind(server, 80)

// block操作,监听80端口的socket
while(int connection = accept(server)) {
  // 对于每一个链接,创建一个新的线程去处理这个连接的读和写
  pthread_create(echo, connection)
}

void echo(int connection) {
  char buf[4096];
  while(int size = read(connection, buffer, sizeof buf)) {
    write(connection, buffer, size)
  }
}

以上代码有个问题就是,对于每一个新的连接,都会有一个线程单独去处理,其实线程是非常消耗资源的东西,想象一下,如果一个服务器有几十万的连接,就会有几十万的线程被创建出来,这对服务器的性能要求会非常高。其实对于某一个连接,我们只关心这个连接对应的文件描述符fd(file descriptor),当这个fd有数据时我们就去读,同时我们也可以往这个fd写入数据。所以对于上面这个问题更好的解决方案是(better scalability)利用系统的一些api对某个连接的fd进行操作(read/write/poll), 在*nix系统里面,该系统函数式epoll。以下是使用epoll对上面代码进行优化的结果:

int server = ... // 和上面一样

int eventfd = epoll_create(0)
struct epoll_event events[10]
struct epoll_event ev = { .events = EPOLL, .data.fd = server }
epoll_ctl(epollfd, EPOLL_CTL_ADD, server, &ev)

// 这部分代码类似于Node里面事件循环的代码
while ((int max = epoll_wait(eventfd, events, 10, -1))) {
  for(n = 0; n < max; n++) {
    if (events[n].data.fd.fd == server) {
      // Server 收到一个连接
      int connection = accept(server);
      ev.events = EPOLLIN; ev.data.fd = connection;
      epoll_ctl(eventfd, EPOLL_CTL_ADD, connection, &ev);
    } else {
      // 连接接收到Data
      char buf[4096];
      int size = read(connection, buffer, sizeof buf);
      write(connection, buffer, size);
    }
}}

使用系统函数处理网络连接而不是新建线程来处理,这大大提升了代码的性能和节约了资源,其实node里面也是利用epoll等系统函数进行一些异步操作的,所以node具有很好的扩展性。到这里你可能会问是不是所有node的异步操作都是使用这种方式实现的,其实不是,node里面的异步操作可以分成以下两种类型:

  • pollabe: 这就是利用上面这种方式实现的异步方法。主要是和网络连接相关的东西例如net/dgram/http/tls/https等库。注意和文件读写相关的东西不是pollable的。
  • uv thread pool: node 里面实现异步操作的另外一种方法就是利用libuv提供的线程。fs.*相关的,dns.lookup, crypto.pbkdf2方法都是利用线程池来实现多线程。因为利用线程池实现的异步操作会在一个独立于主线程的线程里面执行,所以这些异步操作相对于同步操作会有更好的性能,这就是为什么我们推荐使用async方法而不使用sync方法的原因。当子线程里面的代码执行完后可以通过self-pipe唤醒主线程的event loop 进行下一步操作。

Node 事件循环的不同阶段

上面简叙了Node的异步操作的实现原理(pollable或thread pool),可是开发者大对数时间关心的是Node事件循环都经历了哪些不同的阶段。

以下是node事件循环的一个架构图:
nodejs-event-loop-phase

在上图中,每个盒子代表事件循环的一个阶段,node引擎会按照顺序依次进入每一个事件循环的阶段。每一个事件阶段都会有自己的一个数据结构(可能是queue,也可能是其他数据结构)来储存和这个阶段相关的一些任务。你还可以看到在上图正中间有两个队列,一个nextTickQueue,另外一个是microTaskQueue。nextTickQueue存储的是用process.nextTick注册的回调函数,microTaskQueue存储的是一些resolved后的promise的回调函数。这两个队列不属于任何一个事件循环阶段,但是每个事件循环阶段都可以访问到这两个队列,如果它们发现这两个队列不是空的,就会执行这两个队列里面的回调函数, nextTickQueue比microTaskQueue具有更大的优先级。值得注意的是这两个队列里面的回调函数是as soon as possible优先级的,它们不是在libUV库里面被实现的,而是在node js代码里面实现的。它们会在js和C/C++代码切换时被检查并执行(如果存在),为了便于理解以下是作者的原话:

These two are not really part of the event loop, i.e. not developed inside libUV library, but in node.js. They are called as soon as possible, whenever the boundary between C/C++ and JavaScript is crossed. So they are supposed to be called right after the currently running operation (not necessarily the currently executing JS function callback).

以下会分别介绍每一个事件阶段:

Timer Phase

这是事件循环的开始阶段。这个事件循环阶段维持了一个存储setTimeout回调函数的min-heap (最小二叉堆),每次从堆中拿出权重最小的setTimeout回调函数,判断这个setTimeout函数是不是超过了其预期执行的时间,如果是就执行这个回调函数,然后再取下一个回调函数进行判断,直到取到的第一个没有超时的任务为止。

Pending i/o callback phase

这个阶段执行的是在pending_queue里面的回调函数。这些回调函数通常都是由先前的一些操作产生的。举个例子,如果你的代码有对一个tcp连接进行写入的操作,并且有指定onSuccess回调函数,当写入操作结束时,这个回调函数就会被放到这个pending_queue里面,同样,哪些onError的回调函数也会被放到这个队列。

Idle, Prepare phase

这个阶段比较特殊,会在每一个事件循环阶段之前被执行,涉及一些node内部的东西,我们开发node代码时不需要考虑这个阶段。

Poll phase

这可能是最重要的事件循环阶段了,在这个阶段里面会接收新的网络连接或者接收文件的数据(self-pipe)。这个阶段的工作可以大体分成以下的部分:

  • 如果watch_queue里面有任务,这些任务会被依次按顺序同步执行,结束条件是这个任务队列空了,或者执行任务的次数超过了当前系统的限制的最大值。
  • watch_queue这个队列变为空时,node会尝试等待一段时间(类似于上面server的例子)以等待新的网络连接的到来。node等待的时间由当前的上下文决定。

Check phase

这个阶段比较简单,就是用来服务setImmediate的,所有setImmediate的回调函数都会在这个阶段被执行。

Close callbacks

涉及到close操作的回调函数会在这里被执行,例如 socket.on('close', () => {}),做一些资源的cleanup操作。

Node 代码的执行

为了更好地理解Node的event loop,我们来看一个实际简单的例子:

// index.js

console.log('hi')

setTimeout(() => {
  console.log('setTimeout')
  process.nextTick('next tick')
}, 0)

setImmediate(() => {
  console.log('setImmediate')
})

这段代码的输出结果是

hi
setTimeout
next tick
setImmediate

或者

hi
setImmediate
setTimeout
next tick

为什么有两种可能,我们来一步一步看

  1. 首先你在命令行输入node index.js后,node会初始化一个事件队列,同时初始化一个变量reventsCount = 0用来记录现在的异步任务数量,然后才开始执行实际的代码。
  2. 主代码执行,输出hi, 然后将第一个setTimeout放进timer phase对应的min-heap,将setImmediate放到check phase对应的queue。这时候因为进行了两个异步操作,reventsCount = 2。
  3. Node代码执行到最后一行js代码时发现reventsCount > 0,开始进入事件循环。
  4. 第一个阶段是timer phase阶段,setTimeout会被拿出来,虽然设置的时间是0, 可是会被置为1(node当timeout等于0或者大于2147483647都会置为1),所以这时候这个任务不一定超时,这是个race condition, 如果已经超时, 第一个setTimeout就会被打印出来,reventsCounts减一,而且一个nextTick的回调函数会被加入到nextTickQueue, reventsCounts加一。
  5. 之前说到nextTickQueue里面的回调函数是ASAP的,所以在进下一个phase前这个nextTick的任务就被执行输出nextTick,reventsCount会减一, 注意这是基于上面那个setTimeout任务已经超时的情况下的结果,如果上面setTimeout没有超时,这个任务也不会被加入到nextTickQueue。
  6. Timer phase过后因为没有pending i/o callbacks,也没有网络连接到来,所以会到check这个阶段,这个阶段会执行setImmediate指定的回调函数, reventsCount - 1。
  7. Close callbacks事件阶段后,node发现reventsCount已经是0了,也就是说没有正在进行的异步任务,系统就会退出这个process。

参考文档

High-performance SQL - MySQL Architecture and History

Connection Management and Security

Each client connection gets its own thread within the server process. The connection's queries execute within that single thread, which in turn resides on one core or CPU. The server caches threads, so they don't need to be created and destroyed for each new connection.

Authentication is based on username, originating host, and password.

Transactions

A transaction is a group of SQL queries that are treated atomically, as a single unit of work. If the database engine can apply the entire group of queries to a database, it does so, but if any of them can't be done because of a crash or other reason, none of them is applied. It's all or nothing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.