据知乎上的陈硕所说,在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步 IO。
怎样理解他这句话呢?《Unix网络编程》这大部经典著作其中有段原文:
POSIX defines these two terms as follows:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
An asynchronous I/O operation does not cause the requesting process to be blocked.
Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.
title: 再次理解同步异步和阻塞非阻塞 date: 2017-07-27 09:30:35 tags:
阻塞非阻塞
从编程角度阐释
异步与同步与单线程或多线程都无关,是以任务的执行顺序有关,是更加高层次的抽象概念,下面以线程举例子:
同步(一个任务的开始必须依赖另一个任务的完成)
单线程:
异步(一个任务的开始不必依赖另一个任务的完成)
所以说,单线程也可以异步,多线程也可以同步。在多线程同步中,一个线程需要等待另一个线程的完成,所以会阻塞当前线程(操作系统会挂起当前线程),也就是多线程同步过程中,一般会有阻塞。单线程同步一般就不会有阻塞,所以在Socket编程中,在创建socket的时候,一般有block和non-block选项,系统会为了等待数据阻塞当前等待数据的线程,并由其他读取数据完毕的线程唤醒等待的recv线程,而继续运行。如果是选择non-block,那么等待数据的线程不会被挂起,无论网络数据是否到达会继续recv返回执行,需要调用者不停地轮询数据是否到达,到达再读取。
block和non-block也只是概念,它的底层大部分是由锁来实现的,是锁概念的延伸和高层次的概念映射,比如读写锁,如果写锁被上锁了,那么读操作会被阻塞(读线程被挂起),等到写锁释放,才能继续读。所以,在谈到block和non-block的时候就会涉及到多线程的概念,肯定底层会有多线程的操作。单线程是不可能有阻塞和非阻塞的概念的。
当然,有人说node.js中的readFile和readFileSync就分别是是异步非阻塞和同步阻塞。而node是单线程的为什么会有block和non-block的概念?因为涉及到阻塞和非阻塞一般是底层可能会有多线程处理IO的操作,nodejs的应用层面是看不到的,因为阻塞的线程是不会自动醒来的,必须要另一个线程来唤醒(Resume)以提示IO的完成。也就是nodejs的应用代码确实是单线程执行的,但是在发起文件IO的时候,肯定会有个IO线程来对文件进行读写,最后通知应用层的js代码。其实以上的说法是不准确的,因为对于文件读写是没有非阻塞这以说法的,因为底层肯定会有阻塞操作等待数据读进缓冲区,还是有锁来阻塞。所以在谈及文件IO的时候我们一般只谈及同步和异步,在谈及网络IO的时候只谈及阻塞和非阻塞。
当然我也觉得同步异步和阻塞非阻塞不能单从字面意义上理解。同步和异步关注的是消息通信机制,所谓同步就是caller主动等待callee的结果,也就是说在没有得到调用结果之前,callee就不返回,一旦callee返回了,一定得到返回结果了。异步正好相反,callee被调用之后,caller对于callee的调用就直接返回了,callee通过状态,或者消息,回调函数等机制来把调用的处理结果通知给caller。
阻塞和非阻塞关注的是caller在等待调用结果(返回值,消息等)时的状态,也就是caller当时的状态。阻塞调用是指callee的被调用结果返回之前,当前的caller线程会被挂起,只有得到结果了,callee才会返回,caller线程被唤醒并继续执行。非阻塞调用是指callee不能得到结果之前,对callee的调用不会阻塞caller的当前线程。
据知乎上的陈硕所说,在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步 IO。
怎样理解他这句话呢?《Unix网络编程》这大部经典著作其中有段原文:
简单来说就是,对Unix-Like的系统来讲:阻塞式I/O(默认),非阻塞式I/O(nonblock),I/O复用(select/poll/epoll)都属于同步I/O,因为它们在数据由内核空间复制回进程缓冲区时都是阻塞的(不能干别的事)。只有异步I/O模型(AIO)是符合异步I/O操作的含义的,即在1数据准备完成、2由内核空间拷贝回缓冲区后 通知进程,在等待通知的这段时间里可以干别的事。
Reactor模式和Proactor模式
通常我们谈论事件驱动思想的时候会提到这两个模式,那么这两个模式到底有什么区别呢?
在Unix标准中的同步IO中的非阻塞IO,也就是NIO(non-block IO),就是基于事件驱动思想的,实现上采用Reactor模式,从程序角度而言,当发起IO的读或写操作时,是非阻塞的;当Socket有流可读或可写入Socket时,操作系统会相应地通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。对于网络IO而言,主要有连接建立、流读取及流写入三种事件,Linux 2.6以后的版本采用epoll 方式来实现NIO。
对于另一种异步IO,也就是AIO(asyn IO),同样是基于事件驱动的思想,实现上通常采用Proactor模式。从程序角度而言,和NIO不同,当进行读写操作时,只需要直接调用API的read或write方法即可。这两种方法均为异步,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序。对于写操作而言,当操作系统将write方法的流写入完毕时,操作系统主动通知应用程序。与NIO比较而言,AIO一方面简化了程序的编写,流的读取和写入都由操作系统来代替完成,另一方面省去了NIO中程序遍历事件通知队列的代价。windows基于IOCP实现了AIO,对,IOCP就是异步的,但是Linux目前只有基于Epoll模拟实现的AIO。显然,Linux从Epoll到AIO有一层转换,就是用同步的Epoll来模拟异步,Epoll是Reactor模式,AIO是Proactor模式。 所以这两个模式的关系,你可以简单理解为,Proactor模式是同步Reactor模式的异步体现,Proactor模式的层次更高,简化了程序编写。从复杂度来将,Reactor更反人类,因为Reactor是通知程序员有数据可以读写了,程序员再主动读写数据。Proactor模式是数据读写完毕了,再主动通知程序员。
需要注意的一点就是Reactor模式一般是单线程实现,几乎所有的Reactor实现的系统都是单线程的,尽管Reactor模式也可以工作在多线程环境下,因为Proactor模式是Reactor的异步形式的变种,所以Proactor模式一般也是单线程实现的,而非多线程。