前言:
自己对协程的概念的理解,源于coobjc的开源。文章参考了其他人对于协程的理解,加以融合贯通,希望能对不了解协程的人给予理解上的帮助。
协程的概念可能很多人不熟悉,第一次听到这个词,可能是这样:
完整的漫画在这里。
看完漫画,可能对协程有了初步的了解,至少知道除了订机票,还能编程~
理解协程,要先明确一个概念:用户态线程
引用一段话加以说明:
- 一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是UI什么的,别一上计算量比较大的玩意就跟死机一样。于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部可以是多cpu并行,也可以是单cpu时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。
- 但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。
- 后来一电脑上有了好几个cpu,好咧,大家都别闲着,一人跑一进程。就是所谓的并行。
- 因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪,资源分配也很难做到相对的公平。所以核心的操作需要陷入内核(kernel),切换到操作系统,让老大帮你来做。
- 有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。
- 如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是用户态线程。
- 从上面可以看到,实现一个用户态线程有两个必须要处理的问题:一是碰着阻塞式I\O会导致整个进程被挂起;二是由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。
了解了用户态线程,就对理解协程更近了一步
协程它能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。普通过程(函数)可看成这个特殊过程的一个特例:只有一个状态,每次进入时局部状态重置。
下面结合coobjc理解一下协程如何控制,切换状态:
上面说的调用状态,我理解成线程的寄存器状态,协程的操作其实就是记录寄存器状态到对应的上下文中,继而通过上线文,控制协程的操作。在coobjc中对应的应该就是coroutine_ucontext_t
对应的启动,切换控制权,恢复就是通过操作coroutine_ucontext_t,核心API如下
coobjc 协程实现的核心在
core/coroutine_context 中,点开头文件,可以看到
//获取协程上下文
extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
//设置协程上下文
extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
//设置协程上下文
extern int coroutine_begin (coroutine_ucontext_t *__ucp);
//创建协程上下文
extern void coroutine_makecontext (coroutine_ucontext_t *__ucp, IMP func, void *arg, void *stackTop);
这里使用汇编实现了上下文的获取、设置:
#if defined(__arm64__) || defined(__aarch64__)
.text
.align 2
.global _coroutine_getcontext
_coroutine_getcontext:
stp x18,x19, [x0, #0x090]
stp x20,x21, [x0, #0x0A0]
stp x22,x23, [x0, #0x0B0]
stp x24,x25, [x0, #0x0C0]
stp x26,x27, [x0, #0x0D0]
str x28, [x0, #0x0E0];
stp x29, x30, [x0, #0x0E8]; // fp, lr
mov x9, sp
str x9, [x0, #0x0F8]
str x30, [x0, #0x100] // store return address as pc
stp d8, d9, [x0, #0x150]
stp d10,d11, [x0, #0x160]
stp d12,d13, [x0, #0x170]
stp d14,d15, [x0, #0x180]
mov x0, #0
ret
.global _coroutine_begin
_coroutine_begin:
ldp x18,x19, [x0, #0x090]
ldp x20,x21, [x0, #0x0A0]
ldp x22,x23, [x0, #0x0B0]
ldp x24,x25, [x0, #0x0C0]
ldp x26,x27, [x0, #0x0D0]
ldp x28,x29, [x0, #0x0E0]
ldr x9, [x0, #0x100] // restore pc into lr
mov x30, #0;
ldr x1, [x0, #0x0F8]
mov sp,x1 // restore sp
ldp d8, d9, [x0, #0x150]
ldp d10,d11, [x0, #0x160]
ldp d12,d13, [x0, #0x170]
ldp d14,d15, [x0, #0x180]
ldp x0, x1, [x0, #0x000] // restore x0,x1
ret x9
.global _coroutine_setcontext
_coroutine_setcontext:
ldp x18,x19, [x0, #0x090]
ldp x20,x21, [x0, #0x0A0]
ldp x22,x23, [x0, #0x0B0]
ldp x24,x25, [x0, #0x0C0]
ldp x26,x27, [x0, #0x0D0]
ldp x28,x29, [x0, #0x0E0]
ldr x30, [x0, #0x100] // restore pc into lr
ldr x1, [x0, #0x0F8]
mov sp,x1 // restore sp
ldp d8, d9, [x0, #0x150]
ldp d10,d11, [x0, #0x160]
ldp d12,d13, [x0, #0x170]
ldp d14,d15, [x0, #0x180]
ldp x0, x1, [x0, #0x000] // restore x0,x1
ret x30
最后列举一点协程的优点
- 跨平台
- 跨体系架构
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
协程没有线程的切换,省去了切换线程需要的开销,这是协程对比多线程的优势。协程不是多线程,自然也舍去了加锁,解锁的操作。但是协程通过代码的控制逻辑,中断,恢复任务。
参考: