目錄
探究 iOS 協程 - 協程介紹與使用(一)
探究 iOS 協程 - coobjc 源碼分析(二)
上一篇講完了協程的概念與使用方式,這一篇我們來分析一下阿里開源協程框架 coobjc 源碼。首先我們先寫一個最簡單的示例程序:
- (void)testCORoutineAsyncFunc {
co_launch(^{
NSLog(@"co start");
// await 后面需要跟 COChan 或者 COPromise
NSNumber *num = await([self promiseWithNumber:@(1)]);
NSLog(@"co finish");
});
NSLog(@"main");
}
// COPromise 模擬了一個異步任務
- (COPromise *)promiseWithNumber:(NSNumber *)number {
COPromise *promise = [COPromise promise:^(COPromiseFulfill _Nonnull fullfill, COPromiseReject _Nonnull reject) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
fullfill(number);
// reject(error); // 如果有錯誤,回調到上層
});
} onQueue:dispatch_get_global_queue(0, 0)];
return promise;
}
以上的代碼會輸出:
main
co start
co finish
co_launch
這里就在主線程開啟了一個協程,現在大家應該特別好奇 await
為什么可以等待異步任務完成?別著急,我們慢慢往下看。
創建協程
首先我們來看一下 co_launch
做了什么事:
/**
Create a coroutine, then resume it asynchronous on current queue.
@param block the code execute in the coroutine
@return the coroutine instance
*/
NS_INLINE COCoroutine * _Nonnull co_launch(void(^ _Nonnull block)(void)) {
// 創建協程
COCoroutine *co = [COCoroutine coroutineWithBlock:block onQueue:nil];
// 開啟協程
return [co resume];
}
co_launch
主要做了兩件事:
- 創建協程,把協程需要執行的 block 作為參數傳進去。
co_launch
默認會在當前線程創建協程。 - 啟動協程。
我們具體來看看如何創建協程:
- (instancetype)initWithBlock:(void (^)(void))block onQueue:(dispatch_queue_t)queue stackSize:(NSUInteger)stackSize {
self = [super init];
if (self) {
// 協程需要執行的 block 賦予屬性 execBlock
_execBlock = [block copy];
_dispatch = queue ? [CODispatch dispatchWithQueue:queue] : [CODispatch currentDispatch];
// 真正創建協程的方法。真正的協程是 coroutine_t 結構體類型,COCoroutine 只是在 OC 層面的一層封裝
coroutine_t *co = coroutine_create((void (*)(void *))co_exec);
// 指定棧空間
if (stackSize > 0 && stackSize < 1024*1024) { // Max 1M
co->stack_size = (uint32_t)((stackSize % 16384 > 0) ? ((stackSize/16384 + 1) * 16384) : stackSize); // Align with 16kb
}
_co = co;
// 讓 coroutine_t 引用 COCoroutine,并設置銷毀函數
coroutine_setuserdata(co, (__bridge_retained void *)self, co_obj_dispose);
}
return self;
}
上面貼出了創建協程的關鍵方法,相關的步驟已經給出了注釋,我們具體來看coroutine_create
:
coroutine_t *coroutine_create(coroutine_func func) {
coroutine_t *co = calloc(1, sizeof(coroutine_t));
co->entry = func;
co->stack_size = STACK_SIZE;
co->status = COROUTINE_READY;
// check debugger is attached, fix queue debugging.
co_rebind_backtrace();
return co;
}
co_rebind_backtrace
這里先忽略。這個方法很簡單,就是創建一個 coroutine_t
結構體,把之前調用者傳入的 co_exec
賦值給 entry
屬性。這里的 co_exec
是一個函數,下面我們來看看這個函數的具體實現:
static void co_exec(coroutine_t *co) {
/* 通過 co_get_obj 拿到 COCoroutine 對象
(之前在創建協程的時候通過 coroutine_setuserdata 把 COCoroutine 對象設置到了 coroutine_t 結構體中)。
這里需要拿到 COCoroutine 的原因是因為協程真正執行的 block 是保存在 COCoroutine 對象中的
*/
COCoroutine *coObj = co_get_obj(co);
if (coObj) {
// 執行之前保存的 execBlock
[coObj execute];
coObj.isFinished = YES;
if (coObj.finishedBlock) {
coObj.finishedBlock();
coObj.finishedBlock = nil;
}
if (coObj.joinBlock) {
coObj.joinBlock();
coObj.joinBlock = nil;
}
//維護父子協程關系
[coObj.parent removeChild:coObj];
}
}
co_exec
主要做的事就是執行保存在 coroutine
上的 block。目前我們的協程就算創建完畢了。
啟動協程
通過上面的分析可以看到,co_exec
是真正執行協程 block 的地方,那么 co_exec
是在什么時候開始執行的呢?回到最開始 co_launch
的地方。co_launch
之后,會立刻調用 [co resume]
,這里 resume
就是真正啟動協程的地方,下面我們來看看 resume
具體實現:
- (COCoroutine *)resume {
// 拿到當前真正運行的協程
COCoroutine *currentCo = [COCoroutine currentCoroutine];
// 判斷是否是當前運行協程的子協程
BOOL isSubroutine = [currentCo.dispatch isEqualToDipatch:self.dispatch] ? YES : NO;
[self.dispatch dispatch_async_block:^{
if (self.isResume) {
return;
}
// 如果是子協程,設置一下父子關系
if (isSubroutine) {
self.parent = currentCo;
[currentCo addChild:self];
}
self.isResume = YES;
// 啟動協程
coroutine_resume(self.co);
}];
return self;
}
要注意,協程是異步追加到隊列中的。如果沒有特別指定隊列,默認會追加到當前線程隊列中。
具體啟動協程在 coroutine_resume
,我們接著往里看:
void coroutine_resume(coroutine_t *co) {
if (!co->is_scheduler) {
// 拿到當前線程的協程調度器
coroutine_scheduler_t *scheduler = coroutine_scheduler_self_create_if_not_exists();
co->scheduler = scheduler;
// 把協程丟到 scheduler 維護的協程集合里(這里的集合是用雙向鏈表實現)
scheduler_queue_push(scheduler, co);
// 如果當前線程有真正運行的協程,把該協程 yield 掉
if (scheduler->running_coroutine) {
// resume a sub coroutine.
scheduler_queue_push(scheduler, scheduler->running_coroutine);
coroutine_yield(scheduler->running_coroutine);
} else {
// scheduler is idle
coroutine_resume_im(co->scheduler->main_coroutine);
}
}
}
在這里需要特別說明一下調度器這個概念。其實在上一篇文章有提到,實現協程的 resume 和 yield 需要一個調度器來控制。調度器每個線程獨有一個,用來調度該線程下的所有協程。同一時間段每個線程下只有一個協程在 running
狀態。
下面的圖很好的詮釋了線程、調度器和協程的關系:
這里的調度器就類似于操作系統在線程調度時候發揮的作用。為什么說協程是一種用戶態的線程
,看到這里想必對這個概念也有了更深刻的理解。
下面我們通過代碼來具體看看調度器是如何創建的。大家還記得上面在 coroutine_resume
方法內部調用了 coroutine_scheduler_self_create_if_not_exists
嗎,我們來看看這個方法具體實現:
coroutine_scheduler_t *coroutine_scheduler_self_create_if_not_exists(void) {
if (!coroutine_scheduler_key) {
pthread_key_create(&coroutine_scheduler_key, coroutine_scheduler_free);
}
void *schedule = pthread_getspecific(coroutine_scheduler_key);
if (!schedule) {
schedule = coroutine_scheduler_new();
pthread_setspecific(coroutine_scheduler_key, schedule);
}
return schedule;
}
可以看到調度器是被存在了 TSD
里,每個線程有且僅有一個,這也就更好的詮釋了上面那張圖片。
說完了調度器,下面我們再回到協程啟動上來。我們當前線程只創建了一個協程,所以不存在 running_coroutine
,那么協程啟動最終會調用到 coroutine_resume_im
來,這個函數有點長,我只截取了啟動相關的部分:
void coroutine_resume_im(coroutine_t *co) {
switch (co->status) {
case COROUTINE_READY:
{
// 分配虛擬內存到 stack_memory
co->stack_memory = coroutine_memory_malloc(co->stack_size);
// 根據虛擬內存地址計算棧頂指針地址
co->stack_top = co->stack_memory + co->stack_size - 3 * sizeof(void *);
// get the pre context
// 在堆上開辟一塊內存,隨后調用 coroutine_getcontext 把當前函數調用棧存入 pre_context。
co->pre_context = malloc(sizeof(coroutine_ucontext_t));
BOOL skip = false;
// coroutine_getcontext 保存了當前函數調用棧,但最主要得是保存 lr 寄存器的地址(下一條指令地址)。
coroutine_getcontext(co->pre_context);
if (skip) {
// when proccess reenter(resume a coroutine), skip the remain codes, just return to pre func.
return;
}
#pragma unused(skip)
skip = true;
free(co->context);
co->context = calloc(1, sizeof(coroutine_ucontext_t));
// 通過 coroutine_makecontext 生成一個協程上下文,跟 coroutine_getcontext 類似,只不過這里是直接用結構體模擬的。
coroutine_makecontext(co->context, (IMP)coroutine_main, co, (void *)co->stack_top);
// setcontext
// 真正開啟協程的函數,這里一執行,就會調用到 coroutine_main 這個函數里。
coroutine_begin(co->context);
break;
}
.........
}
coroutine_resume_im
主要做了三件事:
- 把當前的函數棧保存在
co->pre_context
中(其實就是保存 lr)。 - 生成一個新的 context 保存在
co->context
中。 - 開始執行
co->context
中保存的函數(coroutine_main)。
coroutine_getcontext
、coroutine_makecontext
、coroutine_begin
等被稱為協程族函數,具體實現細節會在后一篇文章討論,這里只需要知道它們的作用就可以。
現在我們知道,協程本身會保存 pre_context
和新建一個 context
,這里也引申出來一個問題:為什么要保存 pre_context
?原因是當我們的協程執行完之后,還需要回到我們想回去的地方。我在哪里設置了 pre_context
,那當我協程執行完之后就可以通過 coroutine_setcontext
回到我當初設置 pre_context
的地方。
到這里大家也不難想象協程是怎么實現異步的同步化表達。在傳統的 block 異步編程中,其實是把異步操作執行完需要回調的函數地址保存在 block 對象內部,然后通過 block 對象調用這個函數:
那么對于協程來說,它通過
coroutine
對象內部保存了當前函數調用棧,當異步執行完之后,取出保存的函數調用棧開始執行原來的函數。剛才說到在調用
coroutine_begin
之后會真正開始執行 coroutine_main
,我們一起來看看這個函數的實現:
static void coroutine_main(coroutine_t *co) {
co->status = COROUTINE_RUNNING;
// 執行協程中保存的 block
co->entry(co);
co->status = COROUTINE_DEAD;
// 執行完畢,回到保存函數棧的地方
coroutine_setcontext(co->pre_context);
}
重點看一下 co->entry(co)
,還記得一開始我們在創建協程的時候賦值給 co->entry
的函數嗎?不清楚的可以回到文章一開始的地方看一下。那么在 coroutine_main
函數調用的時候就真正執行了保存在 co->entry
里的 co_exec
函數,這個函數里會調用保存在 COCoroutine
對象上的 execBlock
,也就是我們文章一開始例子中 co_launch
的 block 參數。
中斷協程
現在,我們的協程已經順利啟動起來了。然后碰到了 await
函數,當前協程會暫停等待 await
之后的異步操作來喚醒,那么我們一起來看看這個函數做了什么:
/**
await
@param _promiseOrChan the COPromise object, you can also pass a COChan object.
But we suggest use Promise first.
@return return the value, nullable. after, you can use co_getError() method to get the error.
*/
NS_INLINE id _Nullable await(id _Nonnull _promiseOrChan) {
id val = co_await(_promiseOrChan);
return val;
}
await
函數很簡單,就是調用了 co_await
,并把返回值返回了出去。我們真正需要看的是 co_await
這個核心函數:
id co_await(id awaitable) {
coroutine_t *t = coroutine_self();
if (t == nil) {
@throw [NSException exceptionWithName:COInvalidException reason:@"Cannot call co_await out of a coroutine" userInfo:nil];
}
if (t->is_cancelled) {
return nil;
}
if ([awaitable isKindOfClass:[COChan class]]) {
COCoroutine *co = co_get_obj(t);
co.lastError = nil;
// 內部會調用 yield 中斷當前協程
id val = [(COChan *)awaitable receive];
return val;
} else if ([awaitable isKindOfClass:[COPromise class]]) {
// 創建 cochan
COChan *chan = [COChan chanWithBuffCount:1];
COCoroutine *co = co_get_obj(t);
co.lastError = nil;
COPromise *promise = awaitable;
[[promise
then:^id _Nullable(id _Nullable value) {
// 當有回調過來,調用 resume 恢復協程中斷
[chan send_nonblock:value];
return value;
}]
catch:^(NSError * _Nonnull error) {
co.lastError = error;
[chan send_nonblock:nil];
}];
// 內部會調用 yield 中斷當前協程
id val = [chan receiveWithOnCancel:^(COChan * _Nonnull chan) {
[promise cancel];
}];
return val;
} else {
@throw [NSException exceptionWithName:COInvalidException
reason:[NSString stringWithFormat:@"Cannot await object: %@.", awaitable]
userInfo:nil];
}
}
COChan內部實現
在上一篇文章中我們有提到 COChan 這個概念和它的一些用法,如果不清楚的話可以再回過去看一下,這里就不再贅述。在 co_await
源碼里可以看到,不管傳進來的 awaitable
對象是 COChan
還是 COPromise
,最終都會調用 COChan
的 receive
方法中斷當前協程,我們先一起來看看 COChan
是如何創建的:
- (instancetype)initWithBuffCount:(int32_t)buffCount {
self = [super init];
if (self) {
_chan = chancreate(sizeof(int8_t), buffCount, co_chan_custom_resume);
_buffList = [[NSMutableArray alloc] init];
COOBJC_LOCK_INIT(_buffLock);
}
return self;
}
COChan
內部會創建一個 co_channel
結構體和一個 _buffList
數組。這里我們也可以看到,COChan
其實也是內部屬性 co_channel
結構體的一層封裝,真正核心邏輯還是 co_channel
在處理,下面我們一起來看看 chancreate
方法:
co_channel *chancreate(int elemsize, int bufsize, void (*custom_resume)(coroutine_t *co)) {
// bufsize == 外面傳進來的 buffCount
co_channel *c;
if (bufsize < 0) {
// 沒有 bufferCount 不需要額外存儲空間
c = calloc(1, sizeof(co_channel));
} else {
c = calloc(1, (sizeof(co_channel) + bufsize*elemsize));
}
// init buffer
if (bufsize < 0) {
queueinit(&c->buffer, elemsize, 16, 16, NULL);
} else {
// bufferCount >= 0 -> expandsize == 0
queueinit(&c->buffer, elemsize, bufsize, 0, (void *)(c+1));
}
// init lock
c->lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
c->custom_resume = custom_resume;
return c;
}
bufsize
是我們外面傳進來的 buffCount,在 co_await
函數中,buffCount 的值是 1
。當 bufsize > 0
的時候,會為 co_channel
結構體分配多余的內存空間。bufsize
這里代表緩沖區最大容量。
為 co_channel
分配完內存空間之后,會初始化 co_channel
中的 buffer
屬性,該屬性是一個 chan_queue
類型結構體:
static void queueinit(chan_queue *q, int elemsize, int bufsize, int expandsize, void *buf) {
// bufsize >= 0, expandsize == 0; bufsize < 0, expandsize == 16
q->elemsize = elemsize;
q->size = bufsize;
q->expandsize = expandsize;
if (expandsize) {
if (bufsize > 0) {
// 為容器分配內存空間
q->arr = malloc(bufsize * elemsize);
}
} else {
if (buf) {
// 這里的 buf 是 co_channel 里的 asend 結構體。
q->arr = buf;
}
}
}
創建 co_channel
主要就是初始化了內部的 buffer
屬性,也就是緩沖區。其余的都比較簡單。要注意在這里當外部傳進來的 BuffCount >= 0 時,expandsize == 0,c->buffer->arr == c->asend。具體為什么要這樣設計,我會在后面給出答案。
講完了 COChan
的初始化,緊接著就會調用 [COChan receive]
,我們一起來看看 receive
內部做了什么。receive
最終都會調到 receiveWithOnCancel:
- (id)receiveWithOnCancel:(COChanOnCancelBlock)cancelBlock {
...
IMP cancel_exec = NULL;
if (cancelBlock) {
cancel_exec = imp_implementationWithBlock(^{
cancelBlock(self);
});
}
uint8_t val = 0;
int ret = chanrecv_custom_exec(_chan, &val, cancel_exec);
if (cancel_exec) {
imp_removeBlock(cancel_exec);
}
co.currentChan = nil;
if (ret == CHANNEL_ALT_SUCCESS) {
// success
do {
COOBJC_SCOPELOCK(_buffLock);
NSMutableArray *buffList = self.buffList;
if (buffList.count > 0) {
id obj = buffList.firstObject;
[buffList removeObjectAtIndex:0];
if (obj == kCOChanNilObj) {
obj = nil;
}
return obj;
} else {
return nil;
}
} while(0);
} else {
// ret not 1, means nothing received or cancelled.
return nil;
}
}
省略了與主流程無關的代碼,重點來關注 chanrecv_custom_exec
:
int chanrecv_custom_exec(co_channel *c, void *v, IMP cancelExec) {
return _chanop2(c, CHANNEL_RECEIVE, v, 1, NULL, cancelExec);
}
最終調用了 _chanop2
,主要關注 CHANNEL_RECEIVE
這個枚舉:
typedef enum {
CHANNEL_SEND = 1,
CHANNEL_RECEIVE,
} channel_op;
CHANNEL_SEND
代表往 chan 里面發送消息,也就是調用 send
或者 send_nonblock
;
CHANNEL_RECEIVE
代表調用了 chan 的 receive
或者 receive_nonblock
。
接下來看一下 _chanop2
:
static int _chanop2(co_channel *c, int op, void *p, int canblock, IMP custom_exec, IMP cancel_exec) {
chan_alt *a = malloc(sizeof(chan_alt));
a->channel = c;
a->op = op;
a->value = p;
// 應該是重復賦值了一次
a->op = op;
// 是否需要 yield 當前協程(如果是調用 nonblock 后綴的方法,canblock == 0)
a->can_block = canblock;
a->prev = NULL;
a->next = NULL;
a->is_cancelled = false;
// send 的時候會傳入 custom_exec
a->custom_exec = custom_exec;
a->cancel_exec = cancel_exec;
int ret = chanalt(a);
free(a);
return ret;
}
這里主要就是創建 chan_alt
結構體,真正的核心邏輯在 chan_alt
:
int chanalt(chan_alt *a) {
int canblock = a->can_block;
co_channel *c;
coroutine_t *t = coroutine_self();
// task = coroutine_t
a->task = t;
c = a->channel;
// 對 co_channel 加鎖
chanlock(c);
// 判斷是否需要執行 alt
if(altcanexec(a)) {
return altexec(a);
}
if(!canblock) {
chanunlock(c);
return a->op == CHANNEL_SEND ? CHANNEL_ALT_ERROR_BUFFER_FULL : CHANNEL_ALT_ERROR_NO_VALUE;
}
// add to queue
altqueue(a);
// set coroutine's chan_alt
t->chan_alt = a;
chanunlock(c);
// blocking.
coroutine_yield(t);
// resume
t->chan_alt = nil;
// alt is cancelled
if (a->is_cancelled) {
return CHANNEL_ALT_ERROR_CANCELLED;
}
return CHANNEL_ALT_SUCCESS;
}
在 chan_alt
內部會首先判斷該 chan_alt
是否能夠執行,其次會判斷是否是 block 類型的函數,在這里會出現這么幾種執行路徑:
- 如果不能執行(緩沖區滿了),并且調用的是
receive_nonblock
或send_nonblock
,那么會直接return
。 - 如果不能執行(緩沖區滿了),并且調用的是
receive
或send
,那么會被coroutine_yield
把當前協程中斷。 - 如果可以執行,那么會調用
altexec
并返回結果。
我們先來看一下 altcanexec
函數:
static int altcanexec(chan_alt *a) {
alt_queue *altqueue;
co_channel *c;
c = a->channel;
// buffer.size 是初始化 COChan 時傳進去的 BuffCount,代表緩沖區的容量
// buffer.count 是 buffer 里實際任務的數量
if(c->buffer.size == 0){
/**
1.未設置 buffer.size 或者 buffer.size == 0 說明需要立即執行 chan 里的任務
2.otherop 對 a->op 取反操作,然后會拿到與 op 相反操作的隊列
比如當前的 op 為 CHANNEL_RECEIVE,那么這里的 altqueue 就是拿到一個
SEND的操作隊列。如果 SEND 隊列里面有任務,證明當前的 RECEIVE 操作是可以執行的;
反之如果當前 op 為 CHANNEL_SEND,如果 RECEIVE 隊列中有任務,那么 CHANNEL_SEND
也是可以執行的。
*/
altqueue = chanarray(c, otherop(a->op));
return altqueue && altqueue->count;
} else if (c->buffer.expandsize) {
// c->buffer.expandsize > 0,代表 buffer.size < 0 的情況。
// 如果設置了 buffer.expandsize,意味著 SEND 可以永遠成功 (await 不會走這里)
// expandable buffer
switch(a->op){
default:
return 0;
case CHANNEL_SEND:
// send always success.
return 1;
case CHANNEL_RECEIVE:
return c->buffer.count > 0;
}
} else{
// buffer.size > 0 的情況
//這里的 c.buffer == c.asend
switch(a->op){
default:
return 0;
case CHANNEL_SEND:
// SEND時,buffer 里任務的數量 < 緩沖區最大容量,可以執行 SEND
return c->buffer.count < c->buffer.size;
case CHANNEL_RECEIVE:
// RECEIVE時,buffer 里有任務就可以執行
return c->buffer.count > 0;
}
}
}
這里忽略 c->buffer.expandsize
中的邏輯,重點來看 c->buffer.size == 0
和 else
兩個分支。關于 buffer.size
和 buffer.count
不太理解的可以看上面 co_channel
創建過程的分析,理解了它們倆的概念,再來看這段邏輯應該不難:
-
buffer.size == 0
(無緩沖區),RECEIVE
會直接取c->asend
,SEND
會直接取c->arecv
。如果隊列里面有任務,那么可以成功。 -
buffer.size > 0
(有緩沖區),如果緩沖區內未達最大容量,SEND
可以成功;如果緩沖區內有任務,RECEIVE
可以成功。
如下圖:
在
buffer.size > 0
這個分支里也可以找到為什么要把 c->buffer
設置為 c->asend
的答案:對于存在緩沖區的情況,SEND
和 RECEIVE
都只需要判斷 SEND
任務隊里中的任務數量,而不需要關心 RECEIVE
任務隊列中的任務數量。看完了上面的分析,大家對于中斷的流程應該比較清楚了:當
await
內部調用 receive
的時候,c->asend
里面是不存在任務的,所以 altcanexec
返回 false
,當前協程會被 coroutine_yield
中斷。
恢復協程
上面說到 receive
會中斷當前的協程,那么當異步任務完成之后,會調用 [COChan send_nonblock:val]
把獲取的到數據 val
傳給 COChan
,在這個過程中就觸發了協程恢復。當調用 send
的時候,a->arecv
內部有任務,altcanexec
返回 true
,會立即執行 altexec
函數:
static int altexec(chan_alt *a) {
alt_queue *altqueue;
chan_alt *other = NULL;
co_channel *c;
c = a->channel;
// 拿到 a->op 取反操作隊列
altqueue = chanarray(c, otherop(a->op));
// 取出雙向鏈表尾部的任務
if(altqueuepop(altqueue, &other)){
int copyRet = altcopy(a, other);
assert(copyRet == 1);
// 拿到 other 上的協程(如果是 SEND 這里就是 RECEIVE 的協程)
coroutine_t *co = other->task;
// co_chan_custom_resume
void (*custom_resume)(coroutine_t *co) = c->custom_resume;
chanunlock(c);
// call back sender
chan_alt *sender = a->op == CHANNEL_SEND ? a : other;
// 如果是 SEND 直接執行 a->custom_exec, 如果是 RECEIVE 執行 other->custom_exec
if (sender->custom_exec) {
// [self.buffList addObject:val ?: kCOChanNilObj];
sender->custom_exec();
}
// 把協程加到當前調度器中,如果該調度器上沒有協程在運行,會立刻 resume 這個協程
if (custom_resume) {
custom_resume(co);
} else {
coroutine_add(co);
}
return CHANNEL_ALT_SUCCESS;
} else {
// altqueue 里沒有任務
int copyRet = altcopy(a, nil);
chanunlock(c);
if (copyRet && a->op == CHANNEL_SEND) {
if (a->custom_exec) {
a->custom_exec();
}
}
return copyRet ? CHANNEL_ALT_SUCCESS : CHANNEL_ALT_ERROR_COPYFAIL;
}
}
這個函數代碼比較多,總結起來就是:
- 根據
c->op
取出反操作隊列尾部的任務。 - 拿到該任務保存的協程對象。
- 如果是
SEND
操作,執行綁定在chan_alt
上的custom_exec
,這個函數主要是這句代碼[self.buffList addObject:val ?: kCOChanNilObj]
,就是把send
后面的參數添加到COChan
的buffList
屬性里。 -
resume
第二步保存的協程對象。
到這里我們就可以知道,當滿足 altcanexec
的條件之后:
- 如果調用
send_nonblock
函數,那么會取出RECEIVE
隊列中的任務,把 send 過來的val
放到buffList
中,然后通過custom_resume
恢復RECEIVE
任務中的協程,恢復之后會從buffList
里面取出剛才 send 傳過來的val
,然后return
出去。 - 如果調用
receive_nonblock
函數,會取出SEND
隊列中的任務,把 send 過來的val
放到buffList
中,恢復RECEIVE
任務中的協程。執行完SEND
協程的代碼后繼續執行return CHANNEL_ALT_SUCCESS
,返回到上層后receive_nonblock
會返回 send 存在buffList
中的值。
到此,整個 await 的流程已經比較清晰了,如下圖:
最后
筆者的這篇文章主要從一個簡單的協程例子開始,按著代碼執行步驟一步一步帶大家分析整個協程執行的流程,大家可以邊看文章邊跟著源碼過一遍加深記憶。整個協程實現異步的同步化表達的過程核心在COChan,也就是一個阻塞的消息隊列。當然還有其它的一些類(比如COActor)沒有在這里展開講,其實原理都差不多,它們的核心都是基于協程的幾個族函數。
在下一篇文章我會繼續帶大家分析這幾個族函數在 ARM64 下的實現。