title: Node.js中的异步
categories: tech
tags:
- 异步
- Node.js
前言
写Node.js也有一段时间了,但一直没有理解Node.Js中的异步,同时我也有了更多的疑问异步回调和同步回调的区别,Node.js采用异步的优势是什么?如何才能优雅的写Node.js的代码?
Node.js的单线程
先来看下Node.js的结构,分为三层,如下:
Node.js 标准库,这部分是由 Javascript 编写的,即我们使用过程中直接能调用的 API。在源码中的 lib 目录下可以看到。
Node bindings,这一层是 Javascript 与底层 C/C++ 能够沟通的关键,前者通过 bindings 调用后者,相互交换数据。实现在 node.cc
-
这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。
- V8:Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript 的关键,它为 Javascript 提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。
- Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。
- C-ares:提供了异步处理 DNS 相关的能力。
- http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。
Node.js 虽然说是用的 Javascript,但只是在开发时使用 Javascript 的语法来编写程序。真正的执行过程还是由 V8 将 Javascript 解释,然后由 C/C++ 来执行真正的系统调用。
真正执行系统调用的其实是 Libuv。之前我们提到,Libuv 本身就是异步和事件驱动的,所以,当我们将 I/O 操作的请求传达给 Libuv 之后,Libuv 开启线程来执行这次 I/O 调用,并在执行完成后,传回给 Javascript 进行后续处理。
这里面的 I/O 包括文件 I/O 和 网络 I/O。两者的底层执行略有不同。从上面的 Libuv 官网的图中,我们可以看到,文件 I/O,DNS 等操作,都是依托线程池(Thread Pool)来实现的。而网络 I/O 这一大类,包括:TCP、UDP、TTY 等,是由 epoll、IOCP、kqueue 来具体实现的。
它的单线程指的是自身 Javascript 运行环境的单线程,Node.js 并没有给 Javascript 执行时创建新线程的能力,最终的实际操作,还是通过 Libuv 以及它的事件循环来执行的。
异步和同步
异步是对进程在和I/O(有文件I/O、网络I/O)交互而言的。在做网络请求时候,假设这个操作需要2s,同步会等待这个2s的请求完成再去做下一个动作;然后异步会直接进行下一个动作。
下面这段代码,输出的是null,因为网络I/O这个请求是异步的,执行到异步相关操作的时候会立即返回。
var statusCode = null;
request('https://www.baidu.com/', function(error, response, body) {
var statusCode = response.statusCode;
});
console.log(statusCode);
为什么Node.js要设计为异步
目前为止我接触的编程语言都是同步,刚开始写 Node.js 那是相当的不习惯,即使现在习惯了还是觉得这样编程很麻烦。既然如此那为何要把 Node.js 设计成异步的呢?
异步并不会少程序的运行时间,不会因为你用了异步,查询数据库的时间就会从10ms减少到5ms,无论是同步还是异步,执行的时间依然是10ms,异步的作用仅仅是提升了应用程序的吞吐量。进一步说,异步 IO 会减少单个请求的时间,去掉单个请求中那些无意义的等待时间(从用户角度来看)。所以单位时间内处理的请求没有变化(从服务器监督看),但每个请求的处理时间却减少了。从这个角度,服务器也节约了一些资源——即维持每个请求的连接消耗的内存。
异步这么麻烦应该怎么写
最简单的实现异步的方法就是回调,但是对于有时序性的,异步多了之后就成了回调陷阱,代码会像下面这个样子,瞬间感觉自己智商跟不上了要:
request('https://www.baidu.com/', function(error, response, body) {
var statusCode = response.statusCode;
request('https://www.baidu.com/', function(error, response, body) {
var statusCode = response.statusCode;
request('https://www.baidu.com/', function(error, response, body) {
var statusCode = response.statusCode;
});
});
});
当然现在有很多解决方案,如下图所示:
Node.js如何实现的异步
nodejs是单线程(single thread)运行的,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数。消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中。
实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
用ajax请求做一个具体说明:
--EOF--