理解同步异步,阻塞非阻塞

/ 转载 / 没有评论 / 40浏览
随便翻开一本Node.js入门书籍的绪论部分,一般都可以看到「异步」、「单线程」、「非阻塞」这样的字眼。因其采用异步非阻塞的模型而构建,Node.js得以能充分利用CPU资源,具有极强的处理高并发请求的能力。

可是到底什么是同步和异步?什么是阻塞和非阻塞?同步就意味着阻塞吗?异步就一定是非阻塞吗?即便是业务经验十分丰富的Node.js程序员,都不一定对这些概念辨别得十分明晰。

本文力求以简明的语言来解释清楚这几个概念并加以区分,但不涉及到Node.js底层的具体实现。

阻塞和非阻塞

从简单的开始,我们以经典的读取文件的模型举例。(对操作系统而言,所有的输入输出设备都被抽象成文件。)

在发起读取文件的请求时,应用层会调用系统内核的I/O接口。

如果应用层调用的是阻塞型I/O,那么在调用之后,应用层即刻被挂起,一直出于等待数据返回的状态,直到系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的其他操作。

如果应用层调用的是非阻塞I/O,那么调用后,系统内核会立即返回(虽然还没有文件内容的数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超出了阻塞和非阻塞的辨别范畴。)

这便是(脱离同步和异步来说之后)阻塞和非阻塞的区别。总结来说,是否是阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即「抽离」去完成其他「任务」的则是非阻塞型的。


同步和异步

阻塞和非阻塞解决了应用层等待数据返回时的状态问题,那系统内核获取到的数据到底如何返回给应用层呢?这里不同类型的操作便体现的是同步和异步的区别。

对于同步型的调用,应用层需要自己去向系统内核问询,如果数据还未读取完毕,那此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(所以同步和异步并不决定其等待数据返回时的状态);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层即可以用取得的数据做其他相关的事情。

而对于异步型的调用,应用层无需主动向系统内核问询,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层即可以接收系统内核返回过来的数据,再做其他事情。

这便是(脱离阻塞和非阻塞来说之后)同步和异步的区别。也就是说,是否是同步还是异步,关注的是任务完成时消息通知的方式。由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用


Node.js 的异步非阻塞模型

完整来说,一个最高效且理想的文件读取异步非阻塞模型应该是这样的:应用层发起调用后系统内核立即返回(还没有文件内容数据),应用层继续做其他无关的事情,在系统内核从磁盘读取完数据之后主动通知应用层任务已完成,应用层此时接收系统内核返回的数据,然后继续做其他相关或不相关的事情。

可以看到,在这个模型中,没有无谓的挂起、休眠与等待,也没有盲目无知的问询与检查,应用层做到不等候片刻的最大化利用自身的资源,系统内核也十分「善解人意」的在完成任务后主动通知应用层来接收任务成果。

Node.js 是不是就是这样实现的呢?是,也不是。

现实总是比理想骨感,系统内核并没有理想中那样「善解人意」。异步模型的内核调用在各个平台上实现不一,而且各有各的问题,所以实际上, Node.js 其实是借助多线程来模拟实现了上述理想的异步非阻塞模型。

有人可能有疑问,前面不是说 Node.js 是单线程的吗?

实际上单线程是对用户(使用 Node.js 进行上层开发的程序员,而不是开发 Node.js 的人员)而言的。Node.js 在底层对多个 I/O 操作是借助多线程实现异步非阻塞的,具体来说,Node.js 总是存在一个主线程,用来管理调度 I/O 线程并进行运算,而其他的线程都是 I/O 线程。I/O 线程在主线程的调度下与系统内核进行交互完成完成 I/O 操作并把数据返回给主线程,而主线程对 I/O 线程的调度就完全是上述异步非阻塞的(至于 I/O 线程是异步还是同步、阻塞还是非阻塞,已经不重要了,因为它不影响主线程的效率,只要它能按时返回预期的数据就行)。我们平时所说的 Node.js 是单线程的,就是指 Node.js 的主线程。I/O 线程完全是对用户屏蔽的,所以用户根本无需关心。

这也解释了为什么我们要避免书写计算密集型或者阻塞的代码,一旦主线程被阻塞,那整个应用就是真的都被阻塞了。

场景举例与总结

最后,再来举一个我们日常的例子来加深对这几个概念的理解。

假设小明需要在网上下载一个软件:

相信看完以上这个案例之后,这几个概念已经能够分辨得很清楚了。

总的来说,同步和异步关注的是任务完成消息通知的机制,而阻塞和非阻塞关注的是等待任务完成时请求者的状态。


从Java编程来看

1.BIO

1.每个请求都需要创建独立的线程。

2.当并发量大时,需要创建大量线程,占用系统资源。

3.连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费

 public static void main(String[] args) throws IOException {
   <span class="cm" style="font-style: italic; color: rgb(153, 153, 153);">/**

* 1.创建一个线程池 * 如果有客户端连接了,就创建一个线程与之通信。 */ ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

   <span class="c1" style="font-style: italic; color: rgb(153, 153, 153);">// 创建ServerSocket

ServerSocket serverSocket = new ServerSocket(8080);

   <span class="k" style="font-weight: 600;">while</span> <span class="o" style="font-weight: 600;">(</span><span class="kc" style="font-weight: 600;">true</span><span class="o" style="font-weight: 600;">)</span> <span class="o" style="font-weight: 600;">{</span>
       <span class="c1" style="font-style: italic; color: rgb(153, 153, 153);">// 监听,等待客户端连接

final Socket socket = serverSocket.accept();

       <span class="n">newCachedThreadPool</span><span class="o" style="font-weight: 600;">.</span><span class="na" style="color: rgb(0, 102, 255);">execute</span><span class="o" style="font-weight: 600;">(</span><span class="k" style="font-weight: 600;">new</span> <span class="n">Runnable</span><span class="o" style="font-weight: 600;">()</span> <span class="o" style="font-weight: 600;">{</span>
           <span class="nd">@Override</span>
           <span class="kd" style="font-weight: 600;">public</span> <span class="kt" style="font-weight: 600; color: rgb(23, 81, 153);">void</span> <span class="nf" style="font-weight: 600; color: rgb(241, 64, 60);">run</span><span class="o" style="font-weight: 600;">()</span> <span class="o" style="font-weight: 600;">{</span>
             InputStream<span style="background-color: initial; font-size: inherit; word-spacing: normal;"> </span><span class="n" style="background-color: initial; font-size: inherit; word-spacing: normal;">inputStream</span><span style="background-color: initial; font-size: inherit; word-spacing: normal;"> </span><span class="o" style="background-color: initial; font-size: inherit; word-spacing: normal; font-weight: 600;">=</span><span style="background-color: initial; font-size: inherit; word-spacing: normal;"> </span><span class="n" style="background-color: initial; font-size: inherit; word-spacing: normal;">socket</span><span class="o" style="background-color: initial; font-size: inherit; word-spacing: normal; font-weight: 600;">.</span><span class="na" style="background-color: initial; font-size: inherit; word-spacing: normal; color: rgb(0, 102, 255);">getInputStream</span><span class="o" style="background-color: initial; font-size: inherit; word-spacing: normal; font-weight: 600;">();</span>
           <span class="o" style="font-weight: 600;">}</span>
       <span class="o" style="font-weight: 600;">});</span>
   <span class="o" style="font-weight: 600;">}</span>

}


2.NIO

1.Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)。

2.Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

3.只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。

4.避免了多线程之间的上下文切换导致的开销。

public static void main(String[] args) throws IOException {
// 创建serverSocketChannel
           ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 创建selector
           Selector selector = Selector.open();
// 绑定一个端口8080.在服务器监听
serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 设置为非阻塞 serverSocketChannel.configureBlocking(false); // 把serverSocketChannel注册到selector 关心事件为OP_ACCEPT serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

       <span class="c1" style="font-style: italic; color: rgb(153, 153, 153);">// 循环等待客户端连接

while (true) { // 一秒没有事件发生,没有事件发生 if (selector.select(1000) == 0) { System.out.println("等待了一秒,无连接"); continue; } // 如果返回的大于0,拿到selectionkey集合 // 如果大于0,表示已回去到关注的事件 // 通过selectionkeys反向获取通道 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); // 根据key对应的通道发生的事件做相应处理 // 有新的客户端来连接了 if (key.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept();

                   <span class="c1" style="font-style: italic; color: rgb(153, 153, 153);">// 将socketChannel设置为非阻塞

socketChannel.configureBlocking(false); System.out.println("客户端连接成功======"); // 注册到selector上, 关注事件为读,给socketChannel关联一个Buffer socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } if (key.isReadable()) { // 通过key 反向获取对应的channel SocketChannel channel = (SocketChannel) key.channel(); // 获取到管理的Buffer ByteBuffer buffer = (ByteBuffer) key.attachment(); channel.read(buffer); System.out.println("form客户端" + new String(buffer.array())); } keyIterator.remove(); } } }