同步/异步与阻塞/非阻塞的区别?

文章目录

几个经常容易混淆的概念。特别是像异步与非阻塞,在某些情况下这两个术语常常混用。下面就我谈谈自己对这四个概念的理解,也解释了异步与非阻塞这两个词为何会形影不离。希望对这些概念迷惑,学习 Python 协程、并发相关的朋友有所帮助。

探究事物的基本逻辑

当我们探讨事物的时候,一定要先界定清楚事物的时空界限以及其基本定义,也要先界定清楚探讨的事物/问题的级别或层次(在阿驹看来,世界上的任何东西都是具有分层/分级的,当我们层次分明的时候,很多架构就显得清晰明了)。

就像参考系不确定,怎敢断言一个物体是处于运动态还是静止态?而且当我们提升层次(抽象)和降低层次(分解)来分析问题的时候,一定要回到当初研究的级别上。否则逻辑都有问题,结论又怎会正确。就像研究人,可以分解到各个器官,甚至是各种细胞来研究人,但你最终要回到人本身这个角度,你不能说细胞的特性就是人性。

就讨论计算机程序设计与编写的角度来说,问题的级别与层次是什么呢?最基本的就是要确定我们讨论的是一条 CPU 指令?一个函数?一个类?一个模块?一个服务?一个线程?一个进程?一个操作系统……还是你要下的结论与任何级别的程序都通用?在不同的级别下,有些概念是存在的,有些概念是不存在的。

还有就是界定清楚时空界限。比如常常谈论程序 A 与 B 的性能孰高孰低,架构其孰好孰坏等等。谈论者往往都会站在自己的主观角度,给程序套上自己常接触到的环境,比如没做过分布式应用的,拿着分布式应用在单体应用运行环境和场景中去比。就算功能、架构都定位相同的程序,也得划清一个界限来比较,比如执行时效?生存周期?部署规模?吞吐量?

在我们谈论阻塞、非阻塞、同步、异步这几个概念的时候,也得先划定基本的前提条件,要在同一个级别、同样的时空里来探讨。后文,用“程序单元”这个说法,站在不同的级别,程序单元是不同的。当没有特别说明的时候,你可以认为程序单元就是你心中认为的那一个,当说某程序单元的上一级的时候,你心里要知道,如果先前认为的程序单元是线程,那它的上一级可以是进程,诸如此类。

阻塞与非阻塞

阻塞

阻塞与非阻塞的概念是针对一个程序单元自身而言。

如果一个程序单元的某个操作,在等待这个操作完成的过程中程序单元它自身 无法 继续进行下去做别的事情,那就称这个程序单元在等待该操作时是阻塞的。

这可以是被阻塞的程序单元依赖于别的程序单元,别的程序单元完成它的要求中,它无法继续做任何其他事。 也可以是对硬件设备的依赖(其实在程序看来是对更底层驱动程序的依赖)。常见的阻塞形式有网络 I/O 阻塞,磁盘 I/O 阻塞,用户输入阻塞,CPU 阻塞等等。

站在被阻塞的程序单元自身来讲,是它消耗了时间等待某事情的结果而发生了阻塞。所以,在说到阻塞的时候,心里就需明确知道,完整的一句描述应该是“某程序因等待某操作的结果而阻塞”。 比如一个文件操作函数,因为要等待磁盘 I/O 拿到数据,那么就称为该文件操作函数因等待磁盘 I/O 的结果而发生了阻塞。

非阻塞

非阻塞就是阻塞的反面。即是如果一个程序单元的某个操作,在等待这个操作完成的过程中程序单元它自身 可以 继续进行下去做别的事情,那就称这个程序单元在等待该操作时是非阻塞的。

这里我们就会看到非阻塞并不是在任何程序级别、任何情况下都是存在的。例如在单个函数的级别,上一行是向磁盘索取文件,下一行是对文件内容进行运算,那么当磁盘 I/O 未有结果时,是无法继续下一行的,此时这个函数是不可能存在非阻塞的状态的。

只有当程序单元高到了一定级别,它可以囊括独立的子程序单元,它才可能有非阻塞状态的存在。因为阻塞的操作都在子程序单元中,阻塞的是子程序单元而不会阻塞它本身。例如一个单进程多线程的文件操作程序,其中某个线程在操作一个文件时阻塞了,而别的线程还可以继续运行下去,此时该进程本身还是运行态而非阻塞态,所以该文件操作程序是非阻塞的。

由上可知,在计算机程序的世界里,阻塞是绝对的,非阻塞是相对的。

同步与异步

同步

同步异步的概念是针对至少两个程序单元而言(可以是同级别的,也可以是不同级别的程序单元)。同,一致;异,不一致;只有一个程序单元的世界里,没有别的和它对比,谈何一致或不一致?

多个程序单元之间,通过某种方式通信进行了协调,使它们在相同的时间点的行为或目标一致就称为同步。

在生活中同步的例子就是三军仪仗队行进过程中,他们通过瞄排头兵和身边队友的这种行为来协调自己的动作与他们一致,这就是同步。国旗手听到国歌的某个旋律(信号)就开始撒开国旗并往上升,通过这种行为让国旗的上升与国歌的旋律同步。

可见,同步的重点在于多方之间有信息传递并以此协调一致完成共同目的,而不在于非要多方完成一模一样的事情。

异步

毫不相干的程序单元之间肯定是异步的,多个相干的程序单元之间虽然有某种方式的通信,但过程中无需协调一致也能完成共同目标就是异步的。

我们会发现,真正意义上的异步是比较少的。而我们常见的技术文档中所提到的异步,是指多个相干的程序单元,其中一个对另一个有依赖,发起请求的程序单元不必等待另一个完成所有的需求就得到了一个临时的返回结果,而请求方真正需要的数据是稍后再返回给请求方。为了解耦程序单元之间的直接关系,所以常常引入了第三者,即是所谓的异步框架或者异步机制。

常见的异步机制有回调、事件循环、信号量等,它们也常常会相互结合使用。

由上述可知,现在大多数文档或者大家所说的异步,其主要目的其实是为了让请求方不必因为该次请求而阻塞以提高请求方的工作效率,所以非阻塞与异步两词就如孪生兄弟般形影不离甚至出现了相互代替的现象。 我们也可以得知,绝大多数时候,程序单元的级别能够囊括独立子程序单元的情况下,才会有所谓的异步,比如要借助多协程、多线程、多进程来实现异步程序。

异步编程
以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。

难点

“程序有自己的想法”。因为执行顺序的不可预料性,我们对某个时间点正在发生的事件不可准确预知。尤其在并行情况下就显得更加复杂和艰难。

所以,几乎所有的异步框架都将异步编程模型简化一次只允许处理一个事件。故而有关异步的讨论几乎都集中在了单线程内。一旦采取异步编程,每个异步调用必须“足够小”,不能耗时太久。如何拆分异步任务成了难题。

  • 程序下一步行为往往依赖上一步执行结果,如何知晓上次异步调用已完成并获取结果?
  • 回调(Callback)成了必然选择。那又需要面临“回调地狱”的折磨。
  • 同步代码改为异步代码,必然破坏代码结构。
  • 解决问题的逻辑也要转变,不再是一条路走到黑,需要精心安排异步任务。

并行与并发

并行(parallelism)

并行的关键是程序有同时处理执行多个任务的能力。判断程序是否处于并行的状态,就看同一时刻是否有超过一个“工作单位”在运行就好了。所以,单线程永远无法达到并行状态。

并发(concurrency)

并发指的是程序的“结构”。并发的关键是程序有处理多个任务的能力,不一定要同时。当我们说这个程序是并发的,实际上,这句话应当表述成“这个程序采用了支持并发的设计”。好,既然并发指的是人为设计的结构,那么怎样的程序结构才叫做支持并发的设计?

正确的并发设计的标准是:使多个操作可以在重叠的时间段内进行(two tasks can start, run, and complete in overlapping time periods)。

这句话的重点有两个。我们先看“(操作)在重叠的时间段内进行”这个概念。它是否就是我们前面说到的并行呢?是,也不是。并行,当然是在重叠的时间段内执行,但是另外一种执行模式,也属于在重叠时间段内进行。这就是协程。

补充

尽管有一种趋势认为并行性意味着多个内核,但是现代计算机在许多不同级别上都是并行的。 直到最近,每个内核之所以能够每年保持更快的速度,是因为它们在位和指令级都并行使用了摩尔定律所预测的所有额外晶体管。

位级(Bit-Level )并行

为什么 32 位计算机比 8 位计算机快? 并行性。 如果 8 位计算机要相加两个 32 位数字,则必须按 8 位操作序列进行操作。 相比之下,一台 32 位计算机可以一步完成,并行处理 32 位数字中的 4 个字节。 这就是计算历史使我们从 8 位架构转变为 16 位,32 位以及现在的 64 位架构的原因。 不过,我们将从这种并行性中看到的总收益是有限的,这就是为什么我们不太可能很快看到 128 位计算机的原因。

指令级(Instruction-Level )并行

现代 CPU 使用流水线,乱序执行和推测性执行等技术来实现高度并行。

作为程序员,我们大多数情况下都可以忽略这一点,因为尽管处理器一直在并行处理眼下的事情,但它始终保持着错觉,即一切都是顺序发生的。 然而,这种幻想正在破裂。 处理器设计人员不再能够找到提高单个内核速度的方法。 当我们进入多核世界时,我们需要开始担心指令没有按顺序处理这一事实。 我们将在《七周七并发模型》第 29 页的“内存可见性”中进一步讨论。

数据(Data)并行

数据并行(有时称为 SIMD,“单指令,多个数据”)体系结构能够对大量数据并行执行相同的操作。 它们并不适合所有类型的问题,但是在适当的情况下它们可能非常有效。 最适合数据并行性的应用程序之一是图像处理。 例如,要增加图像的亮度,我们要增加每个像素的亮度。 因此,现代 GPU(图形处理单元)已经发展成为功能非常强大的数据并行处理器。

任务级(Task-Level )并行

最后,我们达到了大多数人认为的并行性-多个处理器。 从程序员的角度来看,多处理器体系结构最重要的区别特征是内存模型,特别是共享的还是分布式的。

总结

“并发”指的是能够让多个任务在逻辑上交织执行的程序设计,“并行”指的是程序在物理上同时运行时的状态。

总结

我们在理解以上概念的时候,一定别以固化的思维去理解,别一提到上述概念想到的就是 I/O,就是单体应用,甚至是还将多个层次混为一谈,那是不可取的。一个程序单元可以在某些情况下是阻塞的,可以在某些情况下是非阻塞的,可以在某些时候是同步的,可以在某些时候是异步的,这都没有确切的定性。

根据上文的解释,大家还可以自行理解一下“异步阻塞”、“异步非阻塞”、“同步非阻塞”、“同步阻塞”这四种模式各自是怎样的情景?为了完成同样的功能,针对不同的应用目的,选择哪种模式才是最合适的?哪些模式是完全没必要存在于任何程序中的?哪些模式是可以被任何程序都可以采用的?在应对大规模并发的时候,这四种模式应该各自如何扩展才能应对挑战?

参考链接

并发与并行的区别是什么? - 知乎
还在疑惑并发和并行? - laike9m’s blog
大话同步/异步、阻塞/非阻塞
关于同步/异步 VS 阻塞/非阻塞的一点体会
同步,异步,阻塞,非阻塞等关系轻松理解
Synchronous vs Asynchronous
I/O Concept – Blocking/Non-Blocking VS Sync/Async
深入理解并发/并行,阻塞/非阻塞,同步/异步
迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO 讲的这么清楚的好文章
怎样理解阻塞非阻塞与同步异步的区别?
关于同步、异步与阻塞、非阻塞的理解