回调和协程:利用同步思路处理异步响应的本质

编程领域的同步和异步

  1. 同步:指一个执行序1在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个执行序将会一直等待下去,直到收到返回信息才继续执行下去;
  2. 异步:指执行序不需要一直等下去,而是继续执行下面的操作,不管其他执行序的状态。当有消息返回时系统会通知指定执行序进行处理。这样在等待操作完成的过渡事件,系统可以有效利用cpu的资源。

从以上定义可以看出同步和异步之间的区别在于主动权的所属,基本上可以理解为:

  • 同步操作主动权在发起者,操作者需要检索指定条件是否满足,如果满足则继续执行,否则继续等待。
  • 异步操作主动权切换,操作者发起异步操作之后,主动权归还系统,当系统满足条件之后,系统会继续调用接下来的处理步骤。

从本质上来看,同步的思维更加接近人的惯性思维,更加容易编写成熟稳定的代码,异步代码更符合CPU硬件运行,更加容易编写高效运行代码。

两者看似是一对相互对立的概念,然而在现代编程领域,除了裸写嵌入式程序,一般情况下,同步和异步操作的区别不再明显,更何况有不少编程手段立意用同步的编程方式编写异步任务处理。

同步和异步是两个相依相对的概念,从表现上来看,不同的范围上一个任务可能表现出二象性。所以讨论一个任务是否是同步任务是需要指定范围的。

操作系统提供的抽象工具

作为计算机所有资源的管理者,操作系统的强大无可置疑。为了实现资源的高效管理,他提供了最基本的异步机制抽象,是最经典的异步处理的实现案例

为了更加高效的利用计算机资源,操作系统基本上都具备进程和线程的概念。进程和线程封装了程序运行需要的资源和代码,也是基本的异步操作实体。

操作系统一般对进程和线程进行抢占式调度,当执行序的时间片消耗完成,或者当执行序访问了指定资源(例如磁盘I/O、Socket)之后,执行序(进程、线程)便会被剥夺CPU的所有权,让渡给其他等待的执行序。

等到条件满足,资源准备完成。系统会通知等待此资源的执行序,回复该执行序的运行。

这就是一个基本异步机制封装,整个处理过程正是异步操作的基本思路,发送异步请求-让渡主动权-系统回调处理过程。然而这整个程序的编写却是同步代码。从程序自身的角度上来看,代码是同步运行的,自己等待条件满足的过程中“The world was stoped”。

这是同步机制和异步机制的统一。所以不必特意的追求异步编程,我们同样能够享受异步操作的好处

同步代码效率低的原因

从操作系统角度上来看,所谓原生异步代码和同步代码本质上没有什么不同,那为什么出现同步代码比专门编写的采用异步API的代码效率低下的现象?

我觉得问题出现在同步代码采用的API颗粒度更小、异步操作将阻塞操作隔离在用户代码空间

从读取文件的角度上来看,基本上同步API以字节为单位,而异步API绝大多数都是以数据块为单位,而且基本上都带有大量缓存,系统调用的次数越少,锁的使用也就越少,效率自然越高。

对于读取文件这类独占资源的操作,代码中调用同步API会立刻陷入等待,将主动权交给系统的线程调度。在操作系统中同时存在数百上千的线程,系统一视同仁,软件所占的CPU时间比例也就很小
而异步API会将请求存入指令队列,系统如果存在其他执行序会立刻切换,直到线程时间片使用完毕,或者系统中不存在可以切换的非阻塞执行序。异步过程中只发生了指令存入队列过程,并不存在阻塞操作,可以将CPU分到的时间片消耗干净,软件以函数回调的方式切换执行序,因此只需要付出函数压栈的成本
异步操作环境的后台守护线程是真正执行阻塞操作的线程,他从队列中获取命令、执行阻塞API操作,等待结果完成,将完成结果存入结果队列。

利用同步思路处理异步响应的原理

现代编程领域追求的是编写效率和运行效率的统一,为了运行效率的最大化,就必须采用异步编程方式,采用更有效率的API接口。

采用异步机制的编程方式基本手段是过程回调。回调函数一多起来,就让人摸不到头脑。在异步操作环境中,拥有运行时级别的缓冲队列,因此能够提供异步操作API,而异步操作API的使用如果采用函数回调的方式会大大增加思维成本。

因此需要将异步操作与同步操作统合起来,操作系统进程和线程在这方面为我们提供了一个实例。也就是再增加一层缓存、融合回调提供路由机制,自行切换执行序2

可以看出正常的实现同步思路处理异步响应需要三层缓存和抽象。

  1. 操作系统抽象硬件的异步执行,提供逻辑上的同步接口、存在操作系统级别的缓存队列。
  2. 异步环境封装同步接口,提供异步操作接口,提供路由机制自由调用回调过程。存在运行时级别的缓存队列。
  3. 用户路由机制封装异步操作API,提供同步逻辑编程体验,存在用户级别的路由层。

从架构级别上来看代码层数量越多,效率越低,异步环境的存在使得API执行期间必须添加上队列等待时间,因而效率偏低。
而从整体表现上来看,由于耗时操作都被转移到守护线程,因此执行线程可以将时间片消耗完全。在CPU时间消耗的整体占比更大,整体时间更长,运行任务更多。
至于切换过程,由于在实际代码中消耗时间的主要是工作代码,切换的过程占比较小,对CPU执行效率影响不大。但是如果每个回调函数非常短小,消耗时间很短,频繁的切换不同的执行序就会造成路由层消耗剧增。严重影响运行效率。

协程的实现

协程也就是所谓的用户态线程,本质上是对于函数回调的封装。

在协程的实现机制中基本上都包含一个状态机。当用户代码发起异步请求的时候,在请求的同时或之前需要注册指定状态的回调过程,当异步请求完成后,状态机会根据状态回调指定的处理过程。

本质上协程并不是一个如同线程和进程的独立运行的实体,他只是逻辑上的被人为组合起来的执行序。由于并不能独立执行,因此他被依附在操作系统的线程上。

由于协程的实现基本上是基于回调函数的语法糖,因此将回调函数与操作系统的进程和线程进行对比是十分不公平的,由于不是真正的物理执行序,将协程与进程和线程比较更加没有道理

基本的协程实现采用一个线程执行所有的用户代码1:N,某些环境(go)采用了N:M的实现方式,允许多个执行序依附于不同操作系统线程。因此golang要求采用管道传递数值,本质上是一个数值阻塞队列,需要采用这种缓冲机制避免线程冲突。

golang的实现较为复杂,需要利用编译器针对性的优化,因此专门发明了一种语言,而不是对于已有语言进行修改。


  1. 为什么采用执行序这个冷僻的概念而不是流行的进程或者线程的概念是因为本文试图从本质上解释同步和异步的概念而不局限于某个特定的环境。 ↩︎

  2. 经典的路由机制根据当前状态自由调用回调函数,产生了多个执行序并发执行的现象,整个过程是发生在同一个操作系统线程中,基本上不存在并行执行。线程才是操作系统执行的基本单位。 ↩︎

你可能感兴趣的:(C/C++,编程经验,操作系统原理,通用编程经验)