同步阻塞、同步非阻塞、异步阻塞、异步非阻塞与 I/O 多路复用、Java NIO 之间的联系
先验知识此处的异步指的是什么同步、异步、阻塞、非阻塞同步阻塞、同步非阻塞、异步阻塞、异步非阻塞一个生动的例子I/O 多路复用总结与补充先验知识
在解释这几个概念之前,需要注意的是:
首先需要知道操作系统层面的同步、异步、阻塞这几个概念的含义。关于这方面的内容,可见笔者的另一篇博客:
同步与异步、并行与并发、阻塞与挂起: /wangpaiblog/article/details/116114098
本文解释的概念至少适用于编程语言层面,但不适用于操作系统层面。原因是,在软件工程中,任何设计都可以进行分层封装。其中,每个中间层的设计对上层与下层来说都是透明的。因此,编程语言层面的“同步、异步、阻塞”与操作系统层面的没有必然联系。
有人喜欢对“同步阻塞、同步非阻塞、异步阻塞、异步非阻塞”的概念,首先区分发送方和接收方,但实际上,无论是发送还是接收,本质上都是一种“请求”的过程,如果不在操作系统层面进行分析,区分这两者实无必要。
同步、异步是针对两个任务(此任务不同于操作系统理论中的作业、进程等,指的只是一个线程中要做的一件事情。本文中的任务都指的是这个。)来说的,其中一个任务为一开始就在执行、所主要关注的,称为主任务,而另一个是与主任务相关的一个任务,称为相关任务。当主任务与相关任务分别位于不同的线程时,称该线程分别为主线程、相关线程。
阻塞是针对主任务所在的线程来说的。当主任务和相关任务位于同一个线程时,不存在“阻塞”,即此情况下不存在同步阻塞、异步阻塞。
但对于下面的情况,主任务和相关任务一定位于不同的线程:
主任务发起的是 I/O 请求
主任务发起的是网络请求
请求是一个期望获得资源的行为,而“同步阻塞、同步非阻塞、异步阻塞、异步非阻塞”描述的是获得期望资源之前的行为,而不是其之后的行为。这四个概念中的“阻塞”不描述在获得资源之后主线程的程序走向。
此处的异步指的是什么
首先需要明白,无论是对“异步”还是“非阻塞”的主线程,都不可能凭空接收到外界、执行没有事先设置的程序。而之所以主线程的行为会受到相关线程的影响,是因为主线程会周期性地调用一种“请求”函数,而此“请求”函数的行为会受相关线程的影响。
其次,对于操作系统,只要其启动,就一直在运行程序。如果没有显式的进程,就运行一个默认的空进程。另外,编程人员不可能事先预计用户本次对本软件的使用时长,然后在软件中设置一个运行时间与其正好相等的一种任务。这就是说,线程在执行时,实际上是在无限循环、周期性地执行一系列任务。于是,可以在每个任务之间插入一个中断点,用于执行额外的操作(任务),以实现对其它线程的交互,这就是异步操作。理由是,中断点之前的任务都已经完成,而任务之间本是不应有任务的(因为任务之间的部分属于空档,没有任何东西),因此,任务之间的中断点是由中断点之前的某任务插入的。由于某种原因,该任务并不想马上执行某操作,而是选择将其移至一系列任务执行完之后的某个任务之间的空档来执行,因此称为异步操作。
同步、异步、阻塞、非阻塞
前面已经指出了关键性的知识点,下面将直接给“同步阻塞、同步非阻塞、异步阻塞、异步非阻塞”的概念。为了更好的说明,笔者做了一张图,如下:
对于主线程上执行的一系列任务,如果其中的某个任务需要与相关线程交互,当其立即暂时当前的任务而发起请求,这称为同步;当该任务将请求的时间安排到某任务完成之后再发起,这称为异步。当发起请求时,直至获得完整的资源之后,不会继续执行现在或之后的任务,这称为阻塞;当发起请求时,立即获得瞬时的结果,然后继续执行现在或之后的任务,如果获得的瞬时资源不是完整的资源,将之后周期性发送类似的请求,直至获得完整的资源,这称为非阻塞。
可以看出,同步与异步的区别在于发起请求的时机,而阻塞与非阻塞的区别在于发起请求后是否对本线程进行暂停。
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
很多读者(包含笔者)都喜欢作者能直截了当地给出概念,而反感拐弯抹角和旁敲侧击。因此,笔者再提炼一下本文核心的四个概念:
同步阻塞:在需要某资源时马上发起请求,并暂停本线程之后的程序,直至获得所需的资源。
同步非阻塞:在需要某资源时马上发起请求,且可以马上得到答复,然后继续执行之后的程序。但如果得到的不是完整的资源,之后将周期性地的请求。
异步阻塞:在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,将暂停本线程之后的程序,直至获得所需的资源。
异步非阻塞:在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,可以马上得到答复,然后继续执行之后的程序。但如果得到的不是完整的资源,之后将周期性地的请求。
【提醒】
有些过分钻研概念的极客可能会对此提出质疑:既然“不马上发起请求”属于异步,但是如果一个线程将 CPU 分配的时间片用完了,此时应该得不到执行,那么同步非阻塞是不是可以归于异步非阻塞呢?
实际上不是这样。一方面,这里的“马上执行”,指的是针对这个程序中相关的代码而言的。基于上一条代码而执行的下一条代码就称为“被马上执行的代码”,而不管这两条代码的执行之间有没有被 CPU 中断。另一方面,评判同步非阻塞
与异步阻塞
的其中一个标准是,如果一个主线程中,依次调用了 N 个同步非阻塞
的方法,则这些方法被执行的次序是不确定的。但如果一个主线程中,依次调用了 N 个异步阻塞
的方法,则当主线程完成当前的任务之后,转而真正执行这些异步方法时,这些方法被执行的次序与之前调用时一致。
一个生动的例子
一个贴切生动的例子可能对理解更有帮助。这里假设了这样的一种情景:笔者正在进行公司安排的一个“cleancode”专项需求(下面简称 cleancode 专项),然后突然对于门禁上报告的一项告警不太理解,笔者想要求助自己的同事(设该同事名为 Bob),于是在公司的通信软件(设该软件名为 contact)上向其发送了此求助消息,并假设笔者每天有减脂的诉求,因此在下午下班后不会立刻去吃饭。
前述的四个概念类比如下:
同步阻塞:笔者在 contact 上给 Bob 发了一条咨询信息,并开启 contact 的消息自动弹出功能。然后笔者暂停手头的工作,翘着二郎腿开始用手机摸鱼,直到手机上弹出 contact 的关于 Bob 的回复信息。
解释:
笔者、笔者的同事 Bob:两个位于不同服务器上的操作系统。
cleancode 专项:正在“笔者操作系统”上运行的一个线程。
笔者放弃工作上的任务:cleancode 专项线程被阻塞。
笔者开始摸鱼:cleancode 专项线程因阻塞而使笔者空闲,笔者通过调度来运行其它无关线程。
contact 的消息自动弹出:在操作系统中,用于唤醒阻塞线程的信号量。
同步非阻塞:笔者在 contact 上给 Bob 发了一条咨询信息,然后笔者继续做 cleancode 专项中的其它内容,并周期性查看 Bob 有没有回复。
异步阻塞:笔者在日程表上记录了这个待办事项,然后笔者继续做 cleancode 专项中的其它内容,最后到下午下班时,笔者在 contact 上给 Bob 发了一条咨询信息,并开启 contact 的消息自动弹出功能。然后笔者暂停手头的工作,翘着二郎腿开始在晚上加班时间用手机摸鱼,直到手机上弹出 contact 的关于 Bob 的回复信息。
异步非阻塞:笔者在日程表上记录了这个待办事项,然后笔者继续做 cleancode 专项中的其它内容,最后到下午下班时,笔者在 contact 上给 Bob 发了一条咨询信息,然后笔者继续加班做 cleancode 专项中的其它内容,并周期性查看 Bob 有没有回复。
I/O 多路复用
说明:
在本小节
I/O 多路复用
中,当名词同步
、异步
单独使用时,它指的是前面先验知识
中提到的“操作系统层面的同步、异步”,而不是同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
中的 “同步、异步”。
同步非阻塞相对于同步阻塞已经有很大的提高了,不过它依然有严重的性能问题。首先,在多线程中,不管它们之间的交互使用的是同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
中的哪种,它们之间都要解决同步
的问题。对于同步非阻塞,由于它是非阻塞的,所以它需要不断地询问对方的状态,这会占用 CPU 的时间。如果有很多线程都选择这么做,系统的性能将受到很大影响。
有一些公共资源请求是经常发生的,比如 I/O 请求。如果使用同步非阻塞的方式来请求,一个问题就是,这些线程彼此之间互不了解对方的情况,在发出请求时没有全局意识。比方说,如果一个临界资源有很多线程都在请求,在这种情况下,应该会有很多线程一定请求不到。但是它们并不知道这一点,因此仍然发出这种请求,这显然是一种浪费行为。那既然这个需求比较热门,那么能不能类似于线程池一样,寻找一个管理公共资源的中介者来协调这种资源的分配呢?
答案是可以的。可以构建一个中介者来接收所有的请求,然后将这些资源按一定的算法来为这些请求作分配。这实际上是一种 Reactor 模式。由于使用了一个中介者来管理这些请求,因此发出这个请求的请求方就不需要周期性地查询资源的状态,它可以交给中介者一个回调方法来令其帮忙实现同步。不过,因为不进行周期性地查询,所以这是一种阻塞查询。另外,对请求者来说,因为资源是由中介者来分配,其不能保证何时调用这个回调同步方法,所以这个过程是一种异步阻塞请求。
I/O 多路复用(I/O multiplexing)正是这样的一种模式。它使用中介者来接收所有的请求,并轮询和管理相应的 I/O 资源。因此,I/O 多路复用是一种异步阻塞 I/O。Java 中的 NIO 正是基于 I/O 多路复用技术。Java NIO 中的 N,指的是 New,而不是 Non-blocking(非阻塞)。很多人分不清这一点,因为他们把异步与非阻塞混为一谈。
总结与补充
用一句话概括本文的概念:
异步:把事情推到以后去做
阻塞:专心做一件事情
同步非阻塞:一边做一件事情,一边做另一件事情(一心二用)
异步阻塞:把问题推到以后专心处理
同步阻塞:马上专心做一件事情
异步非阻塞:把问题推到以后时不时处理一下
线程在被阻塞时,CPU 会将时间片分给其它线程。而当线程发出非阻塞线程请求时,它以后还要周期性地请求,这同样会占用 CPU 时间。因此,不能一味地认为异步非阻塞一定优于同步非阻塞。同样是异步非阻塞,底层实现不同,效率也不同。将程序中的所有代码都改成异步非阻塞,也未必可以提高系统的整体性能。哪种性能最好要取决于具体实现,而不是几个用于装蒜的术语。
Java 中没有异步关键字,所以一般情况下,Java 代码都是同步的,Java 中只有同步阻塞和同步非阻塞。但异步代码可以通过同步代码设计出来,所以 Java 中也可以设计出异步方法。
JavaScript 有异步关键字,但 JavaScript 是单线程的,所以 JavaScript 中只有同步阻塞和异步阻塞。
我们平常单独使用的“同步”一词,实际上指的是这里的同步阻塞,而“异步”指异步非阻塞。
在操作系统层面,只有单独的同步与异步、阻塞与非阻塞的说法。此时的同步与异步的含义主要有以下几个:
同本文的同步阻塞、异步非阻塞。
同步:强调两个程序的运行彼此有逻辑、时间上的先后关系。
异步:强调两个程序的运行彼此相对独立。