coswift原理分析

最近看同事朋友圈说阿里开源了一个协程框架,没听过协程的我一脸懵逼,但对于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public func co_launch(queue: DispatchQueue? = nil, stackSize: UInt32? = nil, block: @escaping () throws -> Void) -> Coroutine {
let co = Coroutine(block: block, on: queue, stackSize: stackSize)
return co.resume()
}

public func await<T>(promise: Promise<T>) throws -> Resolution<T> {
if let _ = Coroutine.current() {

let chan = Chan<Resolution<T>>()

promise.then(work: { (value) -> Any in
chan.send_nonblock(val: Resolution<T>.fulfilled(value))
}).catch { (error) in
chan.send_nonblock(val: Resolution<T>.rejected(error))
}

chan.onCancel = { (channel) in
promise.cancel()
}

return try chan.receive()

} else {
throw COError.invalidCoroutine
}
}

// demo

1
2
3
4
5
6
7
8
9
10
11
12
co_launch {
let result = try await {
co_fetchSomethingAsynchronous()
}
switch result {
case .fulfilled(let data):
print("data: \(data)")
break
case .rejected(let error):
print("error: \(error)")
}
}

在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开始分析

  1. 在开启一个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执行.

  2. 执行main协程的coroutine_ resume_ im,刚开始的main协程status是Ready状态,在该方法中通过coroutine_ getcontext方法将当前的线程寄存器的值设置到main协程的pre_ context中,然后通过coroutine_ begin方法设置当前线程寄存器的值为main协程的context。(函数入口,函数参数,栈地址rbp和rsp等等)

  3. 开始执行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函数指针。

  4. 接着调用了await方法,上面源码给出了实现。在await方法中,调用了promise.then方法,这个方法是异步的,即切换到其他线程执行网络,IO操作,接着执行了chan.receive()方法。

  5. 执行chan.receive()方法,最后会调用coroutine_yield方法,在该方法中会将当前线程的执行状态存储到当前协程(上面的child Coroutine)的context中,然后设置该协程的status为暂停状态。然后将线程的寄存器状态切换回child Coroutine的上一个context(pre_ context,即main协程的运行状态),这时候该线程开始继续执行main协程的函数。实现了child Coroutine的挂起,即demo中的block“运行暂停”了。

  6. 在主协程中的while循环中,接下来没有其他要执行的Coroutine,线程的寄存器状态又会切回线程没进入协程刚开始的时候(即current_ context),此时scheduler的running_coroutine为nil。

  7. 接着异步线程回调回来了,上面源码显示执行了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之前”暂停”运行状态。

  8. 接着就会运行demo中block中的switch result方法。然后child Coroutine的entry运行完,接着走到coroutine_ main的co->status = COROUTINE_DEAD方法(别忘了,所有的协程都是从coroutine _main函数开始执行的),将child Coroutine状态设置成Dead,然后在将线程的寄存器状态切换回main Coroutine(child Coroutine->pre _ context)的运行状态。

  9. 在main Coroutine的While循环过程中,没有child Coroutine了,又将线程的寄存器值切回了最开始没进入协程开始的时候(即current_ context), 即又回到到该线程正常运行的流程。

之后再补个流程图吧。。。