在解释这几个概念之前,需要注意的是:
首先需要知道操作系统层面的同步、异步、阻塞这几个概念的含义。关于这方面的内容,可见笔者的另一篇博客:
同步与异步、并行与并发、阻塞与挂起:
https://blog.csdn.net/wangpaiblog/article/details/116114098
为了描述简洁,本文会在必要的时机将“同步阻塞、同步非阻塞、异步阻塞、异步非阻塞”简称为本文的四个概念。
本文解释的概念至少适用于编程语言层面,但不适用于操作系统层面。原因是,在软件工程中,任何设计都可以进行分层封装。其中,每个中间层的设计对上层与下层来说都是透明的。因此,编程语言层面的“同步、异步、阻塞”与操作系统层面的没有必然联系。
有人喜欢对 本文的四个概念
,首先区分发送方和接收方,但实际上,无论对于发送方还是接收方,都需要在某个时候主动获取资源,都是一种函数调用。因此,就概念上来讲,这些概念跟是发送方还是接收方没有必然关系。发送与接收是通信过程中必然同时存在的东西,不需要单独分类讨论。
本文的四个概念
都是站在主线程的角度上,在调用、定义或传入某个函数的那一刻,评价那个函数属于这 四个概念
中的哪一种。站在不同线程的角度上,评价结果会有所不同。
请求是一个期望获得资源的行为,而 本文的四个概念
描述的是获得期望资源之前的行为,而不是其之后的行为。这四个概念中的“阻塞”不描述在获得资源之后主线程的程序走向。
首先需要明白,无论是对“异步”还是“非阻塞”的主线程,都不可能凭空接收到外界、执行没有事先设置的程序。而之所以主线程的行为会受到相关线程的影响,是因为主线程会周期性地调用函数来与相关线程交互。
其次,对于操作系统,只要其启动,就一直在运行程序。如果没有显式的进程,就运行一个默认的空进程。因此,对于一个进程或线程,可以视为在其生命周期内一直在执行一系列函数。编程中可以使用 IoC 注入等技术在程序的任意阶段动态地插入任意的函数,从而任意控制一个函数的执行时机。一个函数的执行时机与传入时不同就叫异步执行。
前面已经指出了关键性的知识点,下面将直接给 本文四个概念
的定义。为了更好的说明,笔者做了一张图,如下:
对于主线程上执行的一系列函数,当其中的某个函数 A 需要与相关线程交互,而调用了另一个函数 a 时:
如果立即暂时当前的函数 A,去执行函数 a,这称为同步。
如果没有去执行函数 a,而是将执行函数 a 的时机安排在未来的某个时间,然后马上继续执行函数 A,这称为异步。
当执行函数 a 时,直至获得完整的资源之前,都暂停执行当前函数,这称为阻塞。
当执行函数 a 时,立即获得瞬时的结果,然后马上继续执行当前函数。如果获得的瞬时资源不是完整的资源,之后周期性发送类似的请求,直至获得完整的资源,这称为非阻塞。
可以看出,同步与异步的区别在于函数调用的时机,而阻塞与非阻塞的区别在于发起请求后是否对本线程进行暂停。
很多读者(包含笔者)都喜欢作者能直截了当地给出概念,而反感拐弯抹角和旁敲侧击。因此,笔者再提炼一下 本文的四个概念
:
同步阻塞:在需要某资源时马上发起请求,并暂停本线程之后的程序,直至获得所需的资源。
同步非阻塞:在需要某资源时马上发起请求,且可以马上得到答复,然后继续执行之后的程序。但如果得到的不是完整的资源,之后将周期性地的请求,直至获得所需的资源。
异步阻塞:在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,将暂停本线程之后的程序,直至获得所需的资源。在获取资源之后,使用共享信号量、异步回调等方式将结果异步反馈。
异步非阻塞:在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,可以马上得到答复,然后继续执行之后的程序。但如果得到的不是完整的资源,之后将周期性地的请求。在最终获取到资源之后,使用共享信号量、异步回调等方式将结果异步反馈。
一个贴切生动的例子可能对理解更有帮助。这里假设了这样的一种情景:笔者正在进行公司安排的一个“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 有没有回复。
前面已作说明,同步阻塞与异步阻塞的区别只在于执行时机不同。既然同步阻塞与异步阻塞函数都只使用一个线程,那异步阻塞相对于同步阻塞的意义是什么?为什么要使用异步阻塞?
首先,从 运筹学
的角度来说,合理的安排任务的执行时机是可以提高效率的,读者通过生活经验就可以判断出来。
其次,异步与同步不同,异步是 声明式编程
、响应式编程
、函数式编程
、面向方面编程
的必要技术。和 命令式编程
不同,异步编程更优雅、可读性更强。尤其是对框架化的编程来说,异步编程可以让开发人员忽略不重要的细节,大大提高开发效率。如果异步函数是以闭包的形式提供的,还具有闭包自带的强大功能,比一般的同步函数在编程上要方便很多。
但是,这只限于框架上的编程。对于不使用框架的代码,使用异步会让程序流程走向变得复杂。此外,异步代码的调试难度通常远大于同步代码。如果异步函数使用了多线程技术,则编程不当会导致子线程无声吞掉异步函数的异常,从而主线程无法捕获这个异常。对于有些编程语言,函数内定义的局部非静态异步函数会有内存泄漏的风险。开发人员需要根据自己的实际需要选择技术。
【附】
前面已经介绍了 本文的四个概念
,现在来判断某个函数究竟是它们之中的哪一种。这需要先将对该函数的操作分为 调用
、定义
与 传入
。通常不需要特别提及这几个概念,因为这过于简单,但对于某些编程语言及框架,有时候会有意想不到的情况。
函数的调用
:在很多编程语言中指的是使用点运算符 .
来调用某个函数。
函数的定义
:这指的是某个函数的代码所在处。
函数的传入
:在很多编程语言中,这指的是使用一种函数载体,把函数当成一种数据在实参中传递,如 lambda
表达式、函数指针、函数匿名对象等等。在函数调用时,实参为函数就称为函数传入。
【注意】
如果在函数 a 调用时,实参为函数 b(a、b 可以为同一个函数),则对函数 a 来说是函数调用,对函数 b 来说是函数传入。
如果一个函数是通过直接调用的方式使用的,则这个函数是同步函数。
如果这个同步函数的调用会阻塞调用线程,则为同步阻塞函数,否则为同步非阻塞函数。
如果一个函数只需要在代码的特定处定义,而不需要显式地调用或传入该函数,则该函数是异步函数。
在此基础上,选取一个线程作为主线程,如果这个函数在执行之时会阻塞主线程,则为异步阻塞函数,否则为异步非阻塞函数。
这个主线程的选取不是任意的,在很多领域都有默认的选择方式。比如,在 UI 应用中,主线程默认选为 UI 线程。
如果一个函数只是显式地传入,而没有显式地调用,则这个函数一般是异步的。
如果这个函数是传入时马上调用,这种情况就不太好定义了。这主要是看这个函数是否真的是"马上"调用。
如果是,则这个函数可以认为是同步函数。
如果这个函数是在很多代码之后才调用,这个时候可以认为是同步,也可以认为是异步。需要开发者根据自己的理解自行判断。
如果这个异步函数在执行之时会阻塞传入线程,则为异步阻塞函数,否则为异步非阻塞函数。
【注意】
假设在函数 a 调用时,实参为函数 b(a、b 可以为同一个函数)。如果函数 a 是同步阻塞的,且会阻塞到函数 b 调用结束,则不能认为函数 b 也是同步阻塞的。判断时一定要分清楚判断对象是谁。
下面是一些简单粗糙但很有用的经验结论:
普通函数默认是同步阻塞函数
回调函数一般是异步函数
UI 回调函数一般是异步阻塞函数
多线程函数默认是异步非阻塞函数
委托闭包通常是同步阻塞函数
因为回调函数通常是异步的,所以一般无法确定其执行时机。但如果这些回调函数都运行于同一个线程,则运行这些回调函数就会阻塞那个线程。这就是为什么在 UI 领域中,不能在 UI 回调编写耗时代码。因为所有主流领域的 UI 线程全是单线程的,运行耗时回调会导致 ANR(Application Not Response 应用无响应)问题。
因为目前主流语言启动新线程的方式基本都是传入一个回调,且这个回调不一定会马上执行(尤其是使用了 线程池
),所以多线程函数默认是异步非阻塞函数。但是,如果多线程之间使用某种方式实现了与主线程之间的数据同步,那么这些函数就会变成同步阻塞或异步阻塞函数。(此处“数据同步”中的“同步”,与“同步阻塞”中的“同步”不是同一个意思。“数据同步”中的“同步”,指的是数据的一致性,而“同步阻塞”中的“同步”,指的是程序运行先后次序的逻辑性)
单线程一般不可能实现非阻塞,所以一般单线程只有同步阻塞和异步阻塞。但有时候根据角度和技术方案的不同,单线程也可以实现非阻塞。比如,如果每次都将一种耗时任务安排在新建线程中执行,主线程每次只是询问该任务是否完成,而不负责执行该任务,且询问任务是否完成几乎无耗时,则此时主线程的这个询问函数是同步非阻塞的。主线程这一个线程肯定是单线程的,但这个任务是由几个线程一起完成的。这就不太好回答这个非阻塞是不是仅通过单线程实现的。因此,需要根据场景来定义概念,不能死记结论,也不要玩文字游戏。
使用多线程技术可以实现同步、异步、阻塞、非阻塞其中的任意几种。因此,一个方法是不是异步、非阻塞,需要根据具体代码逻辑来判断。回调方法未必就是异步的,同步阻塞回调通常称为委托闭包。委托闭包通常在传入时就在本线程执行,因此使用这种闭包不需要解决线程同步问题。
【附】
回调、钩子、句柄的区别:https://blog.csdn.net/wangpaiblog/article/details/131028253
代理、委托、打桩的区别:https://blog.csdn.net/wangpaiblog/article/details/130691093
如何将一个同步阻塞的函数变成一个异步非阻塞的函数呢?通过本文前面的叙述,可以发现实现这种看起来“高大上”的东西其实很简单。
任何编程语言实现这一点只需要两个必要的技术支持:多线程、队列。
当然,最好还是能支持一种所谓的 lambda 表达式
,不过这不是必要的,但是能让编程变得简单且优雅。而且为了编程简单高效,这种队列最好还是一种 阻塞队列
。阻塞队列是一种特殊的队列,从这个队列中取数据时,如果这个队列为空,这种取操作就会陷入阻塞,直到队列有新的元素进来。使用阻塞队列就可以避免频繁扫描这个队列。
为了行文的方便,不妨就将这里要编写的库称之为 Sky,并将 Sky 使用方传入的函数称之为任务。
首先,需要在 Sky 内部放置一个队列,用于存放传入的任务。然后对外暴露一个提交任务的函数即可。如何执行这个队列中的任务呢?这可以有很多策略。比如,可以在内部添加一个线程池,在队列为空且有空闲线程的时间自动执行。或者对外暴露一个任务启动函数,让 Sky 的使用方自行决定任务执行的时机等等。
这样一来,Sky 的使用方直接传入一个函数就可以让一个同步阻塞的函数变成一个异步非阻塞的函数。如果这个语言不支持多线程,则可以实现的是同步阻塞函数向异步阻塞函数的转化。
一个库编写好了,如果想添加更丰富的功能,就需要将其框架化。笔者之前已经在其它一些博客中详细讲述了一个库的框架化,这里只是简单描述一下,讲解这个不是本文的重点,而且对不同的编程语言,实现也不同。
每个任务的执行都有起点和终点。比方说,可以抽取 Sky 中每个任务的生命周期,在每个生命周期的切入点上插入用户自定义的回调,就可以监控每个任务的执行,并可以统计每个任务的执行时间、决定任务执行发生异常时的处理方案、设置任务执行前后自动触发某些行为、监听某些特殊事件等等。
用一句话概括本文的概念:
异步:把事情推到以后去做
阻塞:专心做一件事情
同步阻塞:马上专心做一件事情
同步非阻塞:一边做一件事情,一边做另一件事情(一心二用)
异步阻塞:把问题推到以后专心处理
异步非阻塞:把问题推到以后时不时处理一下
线程在被阻塞时,CPU 会将时间片分给其它线程。而当线程发出非阻塞线程请求时,它以后还要周期性地请求,这同样会占用 CPU 时间。另外,多线程也可以因为数据同步、线程安全等问题而陷入阻塞(因为非阻塞是评价的是主线程的那个函数,因此即便是子线程被阻塞,从主线程看上去,主线程的那个函数仍然是非阻塞的)。
因此,不能一味地认为异步非阻塞一定优于同步阻塞。同样是异步非阻塞,底层实现不同,效率也不同。将程序中的所有代码都改成异步非阻塞,也未必可以提高系统的整体性能。哪种性能最好要取决于具体实现和合理使用,而不是几个基于刻板印象的术语。
单线程一般不可能实现非阻塞,但多线程可以实现同步、异步、阻塞、非阻塞其中的任意几种。
Java 中没有异步关键字,所以一般情况下,Java 代码都是同步的,只有同步阻塞和同步非阻塞。但异步代码可以通过异步队列和函数式编程技术设计出来,所以 Java 中也可以设计出异步函数。
JavaScript 有异步关键字,但 JavaScript 是单线程的,所以 JavaScript 中只有同步阻塞和异步阻塞。
我们平常单独使用的“同步”一词,实际上指的是这里的同步阻塞,而平常单独使用的“异步””一词,指的是同步非阻塞、异步阻塞、异步非阻塞这三个其中的一种。
在操作系统层面,只有单独的同步与异步、阻塞与非阻塞的说法。此时的同步与异步的含义主要有以下几个:
同步:强调两个程序的运行彼此有逻辑、时间上的先后关系。
异步:强调两个程序的运行彼此相对独立。
同步:同本文前述的同步阻塞。
异步:同本文前述的同步非阻塞、异步阻塞、异步非阻塞这三个其中的一种。