最近看同事朋友圈说阿里开源了一个协程框架,没听过协程的我一脸懵逼,但对于demo中的await实现的效果非常好奇。我就看了下协程的源码(主要是协程的await实现原理)。
在分时操作系统中(单核),线程的切换,往往会伴随着寄存器内容的切换(上下文的切换),上下文的切换,当前CPU执行的任务也就改变了。而对于协程,在单线程内部自己实现了一套多任务机制,将单线程的任务分割成多个(协程),通过切换当前线程的寄存器状态,达到单线程中多协程并发的效果。
设计
coroutine _scheduler _t
一个线程有一个coroutine_ scheduler_ t对象,通过pthread_setspecific设置(runloop对象也是喔,但是复杂些)。一个scheduler对象结构有一个main协程,有一个running协程, 还有个协程队列coroutine _queue,这个queue的设计有点类似libdispatch的queue的设计,只保存着第一个和最后一个协程(任务)。
coroutine(协程)
entry: 函数执行入口
context: 当前协程运行中寄存器值(一个指向coroutine_ ucontext_ re类型的指针)
pre_context: 上一个协程运行时的寄存器值
status: 当前协程的状态
核心汇编代码的目的(参数就一个,是x0,void指针, coroutine _ucontext _t结构)
x0: (某个协程的context或者pre _context)
_coroutine _makecontext(初始化x0结构中的各个变量)
_coroutine _getcontext (将当前线程运行的寄存器的值存储到x0)
_coroutine _setcontext (将当前线程运行中寄存器的值设置为x0的值)
_coroutine _begin (同set _context)
OK,直接一步一步分析源码吧
// 源码
1 | public func co_launch(queue: DispatchQueue? = nil, stackSize: UInt32? = nil, block: @escaping () throws -> Void) -> Coroutine { |
// demo
1 | co_launch { |
在demo中一共有三个上下文,context(即在一个线程中有3个寄存器状态)
current_ context: 当前线程运行寄存器状态(没进入协程的状态)
main_ Coroutine _ context: 主协程运行寄存器状态(这个状态的任务主要用于调度协程)
child_ Coroutine _ context: demo中block里运行时的寄存器状态(子任务状态)
关系如下
1 | child_ Coroutine _ context -> pre_context = main _ Coroutine _ context -> pre_context = current_ context |
demo开始分析
在开启一个co_launch后,创建了一个scheduler对象,还有一个main Coroutine,和上面源码中实例化的那个Coroutine(child Coroutine)。将child Coroutine和main Coroutine串起来(scheduler的_queue, 一个在头,一个在尾)。child Coroutine是真正的任务(demo中的block),main Coroutine开启了while循环,会一直找scheduler的Coroutine queue的head的Coroutine执行.
执行main协程的coroutine_ resume_ im,刚开始的main协程status是Ready状态,在该方法中通过coroutine_ getcontext方法将当前的线程寄存器的值设置到main协程的pre_ context中,然后通过coroutine_ begin方法设置当前线程寄存器的值为main协程的context。(函数入口,函数参数,栈地址rbp和rsp等等)
开始执行main协程的entry(coroutine _ scheduler_ main方法),在该方法中,找到scheduler的Coroutine queue的head,即child Coroutine,然后执行child Coroutine的coroutine_ resume_ im方法(参考步骤2),这时child Coroutine的pre_ context是main协程的函数执行状态。当前线程的寄存器值为child Coroutine的context。这时候开始执行child Coroutine的entry方法,即demo中的co_ lauch后面的block函数指针。
接着调用了await方法,上面源码给出了实现。在await方法中,调用了promise.then方法,这个方法是异步的,即切换到其他线程执行网络,IO操作,接着执行了chan.receive()方法。
执行chan.receive()方法,最后会调用coroutine_yield方法,在该方法中会将当前线程的执行状态存储到当前协程(上面的child Coroutine)的context中,然后设置该协程的status为暂停状态。然后将线程的寄存器状态切换回child Coroutine的上一个context(pre_ context,即main协程的运行状态),这时候该线程开始继续执行main协程的函数。实现了child Coroutine的挂起,即demo中的block“运行暂停”了。
在主协程中的while循环中,接下来没有其他要执行的Coroutine,线程的寄存器状态又会切回线程没进入协程刚开始的时候(即current_ context),此时scheduler的running_coroutine为nil。
接着异步线程回调回来了,上面源码显示执行了chan.send_nonblock方法。在这个方法中,最后执行了coroutine _add方法,此时会将child Coroutine重新设置回scheduler->Coroutine->queue的head, 并且会重新运行main协程,在main协程的循环中,又将child Coroutine取出,并且执行 coroutine _ resume _ im方法(参考步骤2)。不过这时候的child Coroutine的status为暂停状态,会走第二个case。这时候将main协程的运行状态寄存器值保存到child Coroutine的pre _ context中,然后将线程的寄存器值设置成child Coroutine之前”暂停”运行状态。
接着就会运行demo中block中的switch result方法。然后child Coroutine的entry运行完,接着走到coroutine_ main的co->status = COROUTINE_DEAD方法(别忘了,所有的协程都是从coroutine _main函数开始执行的),将child Coroutine状态设置成Dead,然后在将线程的寄存器状态切换回main Coroutine(child Coroutine->pre _ context)的运行状态。
在main Coroutine的While循环过程中,没有child Coroutine了,又将线程的寄存器值切回了最开始没进入协程开始的时候(即current_ context), 即又回到到该线程正常运行的流程。
之后再补个流程图吧。。。