探究 iOS 協(xié)程 - coobjc 源碼分析(二)

目錄

探究 iOS 協(xié)程 - 協(xié)程介紹與使用(一)
探究 iOS 協(xié)程 - coobjc 源碼分析(二)

上一篇講完了協(xié)程的概念與使用方式,這一篇我們來分析一下阿里開源協(xié)程框架 coobjc 源碼。首先我們先寫一個(gè)最簡單的示例程序:

- (void)testCORoutineAsyncFunc {
    co_launch(^{
        NSLog(@"co start");
        // await 后面需要跟 COChan 或者 COPromise
        NSNumber *num = await([self promiseWithNumber:@(1)]);
        NSLog(@"co finish");
    });
    NSLog(@"main");
}

// COPromise 模擬了一個(gè)異步任務(wù)
- (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);  // 如果有錯(cuò)誤,回調(diào)到上層
        });
    } onQueue:dispatch_get_global_queue(0, 0)];
    return promise;
}

以上的代碼會(huì)輸出:

main
co start
co finish

co_launch 這里就在主線程開啟了一個(gè)協(xié)程,現(xiàn)在大家應(yīng)該特別好奇 await 為什么可以等待異步任務(wù)完成?別著急,我們慢慢往下看。

創(chuàng)建協(xié)程

首先我們來看一下 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)) {
    // 創(chuàng)建協(xié)程
    COCoroutine *co = [COCoroutine coroutineWithBlock:block onQueue:nil];
    // 開啟協(xié)程
    return [co resume];
}

co_launch 主要做了兩件事:

  1. 創(chuàng)建協(xié)程,把協(xié)程需要執(zhí)行的 block 作為參數(shù)傳進(jìn)去。co_launch默認(rèn)會(huì)在當(dāng)前線程創(chuàng)建協(xié)程。
  2. 啟動(dòng)協(xié)程。

我們具體來看看如何創(chuàng)建協(xié)程:

- (instancetype)initWithBlock:(void (^)(void))block onQueue:(dispatch_queue_t)queue stackSize:(NSUInteger)stackSize {
    self = [super init];
    if (self) {
        // 協(xié)程需要執(zhí)行的 block 賦予屬性 execBlock
        _execBlock = [block copy];
        _dispatch = queue ? [CODispatch dispatchWithQueue:queue] : [CODispatch currentDispatch];
        // 真正創(chuàng)建協(xié)程的方法。真正的協(xié)程是 coroutine_t 結(jié)構(gòu)體類型,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,并設(shè)置銷毀函數(shù)
        coroutine_setuserdata(co, (__bridge_retained void *)self, co_obj_dispose);
    }
    return self;
}

上面貼出了創(chuàng)建協(xié)程的關(guān)鍵方法,相關(guān)的步驟已經(jīng)給出了注釋,我們具體來看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 這里先忽略。這個(gè)方法很簡單,就是創(chuàng)建一個(gè) coroutine_t 結(jié)構(gòu)體,把之前調(diào)用者傳入的 co_exec 賦值給 entry 屬性。這里的 co_exec 是一個(gè)函數(shù),下面我們來看看這個(gè)函數(shù)的具體實(shí)現(xiàn):

static void co_exec(coroutine_t  *co) {
    /* 通過 co_get_obj 拿到 COCoroutine 對(duì)象
     (之前在創(chuàng)建協(xié)程的時(shí)候通過 coroutine_setuserdata 把 COCoroutine 對(duì)象設(shè)置到了 coroutine_t 結(jié)構(gòu)體中)。
       這里需要拿到 COCoroutine 的原因是因?yàn)閰f(xié)程真正執(zhí)行的 block 是保存在 COCoroutine 對(duì)象中的
     */
    COCoroutine *coObj = co_get_obj(co);
    if (coObj) {
        // 執(zhí)行之前保存的 execBlock
        [coObj execute];
        
        coObj.isFinished = YES;
        if (coObj.finishedBlock) {
            coObj.finishedBlock();
            coObj.finishedBlock = nil;
        }
        if (coObj.joinBlock) {
            coObj.joinBlock();
            coObj.joinBlock = nil;
        }
        //維護(hù)父子協(xié)程關(guān)系
        [coObj.parent removeChild:coObj];
    }
}

co_exec 主要做的事就是執(zhí)行保存在 coroutine 上的 block。目前我們的協(xié)程就算創(chuàng)建完畢了。

啟動(dòng)協(xié)程

通過上面的分析可以看到,co_exec 是真正執(zhí)行協(xié)程 block 的地方,那么 co_exec 是在什么時(shí)候開始執(zhí)行的呢?回到最開始 co_launch 的地方。co_launch 之后,會(huì)立刻調(diào)用 [co resume],這里 resume 就是真正啟動(dòng)協(xié)程的地方,下面我們來看看 resume 具體實(shí)現(xiàn):

- (COCoroutine *)resume {
    // 拿到當(dāng)前真正運(yùn)行的協(xié)程
    COCoroutine *currentCo = [COCoroutine currentCoroutine];
    // 判斷是否是當(dāng)前運(yùn)行協(xié)程的子協(xié)程
    BOOL isSubroutine = [currentCo.dispatch isEqualToDipatch:self.dispatch] ? YES : NO;
    
    [self.dispatch dispatch_async_block:^{
        if (self.isResume) {
            return;
        }
        // 如果是子協(xié)程,設(shè)置一下父子關(guān)系
        if (isSubroutine) {
            self.parent = currentCo;
            [currentCo addChild:self];
        }
        self.isResume = YES;
        // 啟動(dòng)協(xié)程
        coroutine_resume(self.co);
    }];
    return self;
}

要注意,協(xié)程是異步追加到隊(duì)列中的。如果沒有特別指定隊(duì)列,默認(rèn)會(huì)追加到當(dāng)前線程隊(duì)列中
具體啟動(dòng)協(xié)程在 coroutine_resume,我們接著往里看:

void coroutine_resume(coroutine_t *co) {
    if (!co->is_scheduler) {
        // 拿到當(dāng)前線程的協(xié)程調(diào)度器
        coroutine_scheduler_t *scheduler = coroutine_scheduler_self_create_if_not_exists();
        co->scheduler = scheduler;
        // 把協(xié)程丟到 scheduler 維護(hù)的協(xié)程集合里(這里的集合是用雙向鏈表實(shí)現(xiàn))
        scheduler_queue_push(scheduler, co);
        // 如果當(dāng)前線程有真正運(yùn)行的協(xié)程,把該協(xié)程 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);
        }
    }
}

在這里需要特別說明一下調(diào)度器這個(gè)概念。其實(shí)在上一篇文章有提到,實(shí)現(xiàn)協(xié)程的 resume 和 yield 需要一個(gè)調(diào)度器來控制。調(diào)度器每個(gè)線程獨(dú)有一個(gè),用來調(diào)度該線程下的所有協(xié)程。同一時(shí)間段每個(gè)線程下只有一個(gè)協(xié)程在 running 狀態(tài)
下面的圖很好的詮釋了線程、調(diào)度器和協(xié)程的關(guān)系:

image.png

這里的調(diào)度器就類似于操作系統(tǒng)在線程調(diào)度時(shí)候發(fā)揮的作用。為什么說協(xié)程是一種用戶態(tài)的線程,看到這里想必對(duì)這個(gè)概念也有了更深刻的理解。
下面我們通過代碼來具體看看調(diào)度器是如何創(chuàng)建的。大家還記得上面在 coroutine_resume 方法內(nèi)部調(diào)用了 coroutine_scheduler_self_create_if_not_exists嗎,我們來看看這個(gè)方法具體實(shí)現(xiàn):

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;
}

可以看到調(diào)度器是被存在了 TSD 里,每個(gè)線程有且僅有一個(gè),這也就更好的詮釋了上面那張圖片。
說完了調(diào)度器,下面我們?cè)倩氐絽f(xié)程啟動(dòng)上來。我們當(dāng)前線程只創(chuàng)建了一個(gè)協(xié)程,所以不存在 running_coroutine,那么協(xié)程啟動(dòng)最終會(huì)調(diào)用到 coroutine_resume_im 來,這個(gè)函數(shù)有點(diǎn)長,我只截取了啟動(dòng)相關(guān)的部分:

void coroutine_resume_im(coroutine_t *co) {
    switch (co->status) {
        case COROUTINE_READY:
        {
            // 分配虛擬內(nèi)存到 stack_memory
            co->stack_memory = coroutine_memory_malloc(co->stack_size);
            // 根據(jù)虛擬內(nèi)存地址計(jì)算棧頂指針地址
            co->stack_top = co->stack_memory + co->stack_size - 3 * sizeof(void *);
            // get the pre context
            // 在堆上開辟一塊內(nèi)存,隨后調(diào)用 coroutine_getcontext 把當(dāng)前函數(shù)調(diào)用棧存入 pre_context。
            co->pre_context = malloc(sizeof(coroutine_ucontext_t));
            BOOL skip = false;
            // coroutine_getcontext 保存了當(dāng)前函數(shù)調(diào)用棧,但最主要得是保存 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 生成一個(gè)協(xié)程上下文,跟 coroutine_getcontext 類似,只不過這里是直接用結(jié)構(gòu)體模擬的。
            coroutine_makecontext(co->context, (IMP)coroutine_main, co, (void *)co->stack_top);
            // setcontext
            // 真正開啟協(xié)程的函數(shù),這里一執(zhí)行,就會(huì)調(diào)用到 coroutine_main 這個(gè)函數(shù)里。
            coroutine_begin(co->context);
            
            break;
        }
        .........
}

coroutine_resume_im 主要做了三件事:

  1. 把當(dāng)前的函數(shù)棧保存在 co->pre_context 中(其實(shí)就是保存 lr)。
  2. 生成一個(gè)新的 context 保存在 co->context 中。
  3. 開始執(zhí)行 co->context 中保存的函數(shù)(coroutine_main)。

coroutine_getcontextcoroutine_makecontextcoroutine_begin 等被稱為協(xié)程族函數(shù),具體實(shí)現(xiàn)細(xì)節(jié)會(huì)在后一篇文章討論,這里只需要知道它們的作用就可以。
現(xiàn)在我們知道,協(xié)程本身會(huì)保存 pre_context 和新建一個(gè) context,這里也引申出來一個(gè)問題:為什么要保存 pre_context?原因是當(dāng)我們的協(xié)程執(zhí)行完之后,還需要回到我們想回去的地方。我在哪里設(shè)置了 pre_context,那當(dāng)我協(xié)程執(zhí)行完之后就可以通過 coroutine_setcontext 回到我當(dāng)初設(shè)置 pre_context 的地方。
到這里大家也不難想象協(xié)程是怎么實(shí)現(xiàn)異步的同步化表達(dá)。在傳統(tǒng)的 block 異步編程中,其實(shí)是把異步操作執(zhí)行完需要回調(diào)的函數(shù)地址保存在 block 對(duì)象內(nèi)部,然后通過 block 對(duì)象調(diào)用這個(gè)函數(shù):

image.png

那么對(duì)于協(xié)程來說,它通過 coroutine 對(duì)象內(nèi)部保存了當(dāng)前函數(shù)調(diào)用棧,當(dāng)異步執(zhí)行完之后,取出保存的函數(shù)調(diào)用棧開始執(zhí)行原來的函數(shù)。
image.png

剛才說到在調(diào)用 coroutine_begin 之后會(huì)真正開始執(zhí)行 coroutine_main,我們一起來看看這個(gè)函數(shù)的實(shí)現(xiàn):

static void coroutine_main(coroutine_t *co) {
    co->status = COROUTINE_RUNNING;
    // 執(zhí)行協(xié)程中保存的 block
    co->entry(co);
    co->status = COROUTINE_DEAD;
    // 執(zhí)行完畢,回到保存函數(shù)棧的地方
    coroutine_setcontext(co->pre_context);
}

重點(diǎn)看一下 co->entry(co) ,還記得一開始我們?cè)趧?chuàng)建協(xié)程的時(shí)候賦值給 co->entry 的函數(shù)嗎?不清楚的可以回到文章一開始的地方看一下。那么在 coroutine_main 函數(shù)調(diào)用的時(shí)候就真正執(zhí)行了保存在 co->entry 里的 co_exec 函數(shù),這個(gè)函數(shù)里會(huì)調(diào)用保存在 COCoroutine 對(duì)象上的 execBlock,也就是我們文章一開始例子中 co_launch 的 block 參數(shù)。

中斷協(xié)程

現(xiàn)在,我們的協(xié)程已經(jīng)順利啟動(dòng)起來了。然后碰到了 await 函數(shù),當(dāng)前協(xié)程會(huì)暫停等待 await 之后的異步操作來喚醒,那么我們一起來看看這個(gè)函數(shù)做了什么:

/**
 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 函數(shù)很簡單,就是調(diào)用了 co_await,并把返回值返回了出去。我們真正需要看的是 co_await 這個(gè)核心函數(shù):

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;
        // 內(nèi)部會(huì)調(diào)用 yield 中斷當(dāng)前協(xié)程
        id val = [(COChan *)awaitable receive];
        return val;
    } else if ([awaitable isKindOfClass:[COPromise class]]) {
        // 創(chuàng)建 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) {
              // 當(dāng)有回調(diào)過來,調(diào)用 resume 恢復(fù)協(xié)程中斷
              [chan send_nonblock:value];
              return value;
          }]
         catch:^(NSError * _Nonnull error) {
             co.lastError = error;
             [chan send_nonblock:nil];
         }];
        // 內(nèi)部會(huì)調(diào)用 yield 中斷當(dāng)前協(xié)程
        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內(nèi)部實(shí)現(xiàn)

上一篇文章中我們有提到 COChan 這個(gè)概念和它的一些用法,如果不清楚的話可以再回過去看一下,這里就不再贅述。在 co_await 源碼里可以看到,不管傳進(jìn)來的 awaitable 對(duì)象是 COChan 還是 COPromise ,最終都會(huì)調(diào)用 COChanreceive 方法中斷當(dāng)前協(xié)程,我們先一起來看看 COChan 是如何創(chuàng)建的:

- (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 內(nèi)部會(huì)創(chuàng)建一個(gè) co_channel 結(jié)構(gòu)體和一個(gè) _buffList 數(shù)組。這里我們也可以看到,COChan 其實(shí)也是內(nèi)部屬性 co_channel 結(jié)構(gòu)體的一層封裝,真正核心邏輯還是 co_channel 在處理,下面我們一起來看看 chancreate 方法:

co_channel *chancreate(int elemsize, int bufsize, void (*custom_resume)(coroutine_t *co)) {
    // bufsize == 外面?zhèn)鬟M(jìn)來的 buffCount
    co_channel *c;
    if (bufsize < 0) {
        // 沒有 bufferCount 不需要額外存儲(chǔ)空間
        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 是我們外面?zhèn)鬟M(jìn)來的 buffCount,在 co_await 函數(shù)中,buffCount 的值是 1。當(dāng) bufsize > 0 的時(shí)候,會(huì)為 co_channel 結(jié)構(gòu)體分配多余的內(nèi)存空間。bufsize 這里代表緩沖區(qū)最大容量。
co_channel 分配完內(nèi)存空間之后,會(huì)初始化 co_channel 中的 buffer 屬性,該屬性是一個(gè) chan_queue 類型結(jié)構(gòu)體:

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) {
            // 為容器分配內(nèi)存空間
            q->arr = malloc(bufsize * elemsize);
        }
    } else {
        if (buf) {
            // 這里的 buf 是 co_channel 里的 asend 結(jié)構(gòu)體。 
            q->arr = buf;
        }
    }
}

創(chuàng)建 co_channel 主要就是初始化了內(nèi)部的 buffer 屬性,也就是緩沖區(qū)。其余的都比較簡單。要注意在這里當(dāng)外部傳進(jìn)來的 BuffCount >= 0 時(shí),expandsize == 0,c->buffer->arr == c->asend。具體為什么要這樣設(shè)計(jì),我會(huì)在后面給出答案。
講完了 COChan 的初始化,緊接著就會(huì)調(diào)用 [COChan receive],我們一起來看看 receive 內(nèi)部做了什么。receive 最終都會(huì)調(diào)到 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;
    }
}

省略了與主流程無關(guān)的代碼,重點(diǎn)來關(guān)注 chanrecv_custom_exec

int chanrecv_custom_exec(co_channel *c, void *v, IMP cancelExec) {
    return _chanop2(c, CHANNEL_RECEIVE, v, 1, NULL, cancelExec);
}

最終調(diào)用了 _chanop2 ,主要關(guān)注 CHANNEL_RECEIVE 這個(gè)枚舉:

typedef enum {
    CHANNEL_SEND = 1,
    CHANNEL_RECEIVE,
} channel_op;

CHANNEL_SEND 代表往 chan 里面發(fā)送消息,也就是調(diào)用 send 或者 send_nonblock
CHANNEL_RECEIVE 代表調(diào)用了 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;
    // 應(yīng)該是重復(fù)賦值了一次
    a->op = op;
    // 是否需要 yield 當(dāng)前協(xié)程(如果是調(diào)用 nonblock 后綴的方法,canblock == 0)
    a->can_block = canblock;
    a->prev = NULL;
    a->next = NULL;
    a->is_cancelled = false;
    // send 的時(shí)候會(huì)傳入 custom_exec
    a->custom_exec = custom_exec;
    a->cancel_exec = cancel_exec;
    
    int ret = chanalt(a);
    free(a);
    return ret;
}

這里主要就是創(chuàng)建 chan_alt 結(jié)構(gòu)體,真正的核心邏輯在 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;
    // 對(duì) co_channel 加鎖
    chanlock(c);
    // 判斷是否需要執(zhí)行 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 內(nèi)部會(huì)首先判斷該 chan_alt 是否能夠執(zhí)行,其次會(huì)判斷是否是 block 類型的函數(shù),在這里會(huì)出現(xiàn)這么幾種執(zhí)行路徑:

  • 如果不能執(zhí)行(緩沖區(qū)滿了),并且調(diào)用的是 receive_nonblocksend_nonblock,那么會(huì)直接 return
  • 如果不能執(zhí)行(緩沖區(qū)滿了),并且調(diào)用的是 receivesend,那么會(huì)被 coroutine_yield 把當(dāng)前協(xié)程中斷。
  • 如果可以執(zhí)行,那么會(huì)調(diào)用 altexec 并返回結(jié)果。

我們先來看一下 altcanexec 函數(shù):

static int altcanexec(chan_alt *a) {
   alt_queue *altqueue;
   co_channel *c;
   
   c = a->channel;
   // buffer.size 是初始化 COChan 時(shí)傳進(jìn)去的 BuffCount,代表緩沖區(qū)的容量
   // buffer.count 是 buffer 里實(shí)際任務(wù)的數(shù)量
   if(c->buffer.size == 0){
       /**
           1.未設(shè)置 buffer.size 或者 buffer.size == 0 說明需要立即執(zhí)行 chan 里的任務(wù)
           2.otherop 對(duì) a->op 取反操作,然后會(huì)拿到與 op 相反操作的隊(duì)列
           比如當(dāng)前的 op 為 CHANNEL_RECEIVE,那么這里的 altqueue 就是拿到一個(gè)
           SEND的操作隊(duì)列。如果 SEND 隊(duì)列里面有任務(wù),證明當(dāng)前的 RECEIVE 操作是可以執(zhí)行的;
           反之如果當(dāng)前 op 為 CHANNEL_SEND,如果 RECEIVE 隊(duì)列中有任務(wù),那么 CHANNEL_SEND
           也是可以執(zhí)行的。
        */
       altqueue = chanarray(c, otherop(a->op));
       return altqueue && altqueue->count;
   } else if (c->buffer.expandsize) {
       // c->buffer.expandsize > 0,代表 buffer.size < 0 的情況。
       // 如果設(shè)置了 buffer.expandsize,意味著 SEND 可以永遠(yuǎn)成功 (await 不會(huì)走這里)
       // 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時(shí),buffer 里任務(wù)的數(shù)量 < 緩沖區(qū)最大容量,可以執(zhí)行 SEND
               return c->buffer.count < c->buffer.size;
           case CHANNEL_RECEIVE:
               // RECEIVE時(shí),buffer 里有任務(wù)就可以執(zhí)行
               return c->buffer.count > 0;
       }
   }
}

這里忽略 c->buffer.expandsize 中的邏輯,重點(diǎn)來看 c->buffer.size == 0else 兩個(gè)分支。關(guān)于 buffer.sizebuffer.count 不太理解的可以看上面 co_channel 創(chuàng)建過程的分析,理解了它們倆的概念,再來看這段邏輯應(yīng)該不難:

  • buffer.size == 0(無緩沖區(qū)),RECEIVE 會(huì)直接取 c->asend, SEND 會(huì)直接取 c->arecv。如果隊(duì)列里面有任務(wù),那么可以成功。
  • buffer.size > 0(有緩沖區(qū)),如果緩沖區(qū)內(nèi)未達(dá)最大容量,SEND 可以成功;如果緩沖區(qū)內(nèi)有任務(wù),RECEIVE 可以成功。

如下圖:

image.png

buffer.size > 0 這個(gè)分支里也可以找到為什么要把 c->buffer 設(shè)置為 c->asend 的答案:對(duì)于存在緩沖區(qū)的情況,SENDRECEIVE都只需要判斷 SEND 任務(wù)隊(duì)里中的任務(wù)數(shù)量,而不需要關(guān)心 RECEIVE 任務(wù)隊(duì)列中的任務(wù)數(shù)量
看完了上面的分析,大家對(duì)于中斷的流程應(yīng)該比較清楚了:當(dāng) await 內(nèi)部調(diào)用 receive 的時(shí)候,c->asend 里面是不存在任務(wù)的,所以 altcanexec 返回 false,當(dāng)前協(xié)程會(huì)被 coroutine_yield 中斷

恢復(fù)協(xié)程

上面說到 receive 會(huì)中斷當(dāng)前的協(xié)程,那么當(dāng)異步任務(wù)完成之后,會(huì)調(diào)用 [COChan send_nonblock:val] 把獲取的到數(shù)據(jù) val 傳給 COChan,在這個(gè)過程中就觸發(fā)了協(xié)程恢復(fù)。當(dāng)調(diào)用 send 的時(shí)候,a->arecv 內(nèi)部有任務(wù),altcanexec 返回 true ,會(huì)立即執(zhí)行 altexec 函數(shù):

static int altexec(chan_alt *a) {

    alt_queue *altqueue;
    chan_alt *other = NULL;
    co_channel *c;
    
    c = a->channel;
    // 拿到 a->op 取反操作隊(duì)列
    altqueue = chanarray(c, otherop(a->op));
    // 取出雙向鏈表尾部的任務(wù)
    if(altqueuepop(altqueue, &other)){

        int copyRet = altcopy(a, other);
        assert(copyRet == 1);
        // 拿到 other 上的協(xié)程(如果是 SEND 這里就是 RECEIVE 的協(xié)程)
        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 直接執(zhí)行 a->custom_exec, 如果是 RECEIVE 執(zhí)行 other->custom_exec
        if (sender->custom_exec) {
            // [self.buffList addObject:val ?: kCOChanNilObj];
            sender->custom_exec();
        }
        // 把協(xié)程加到當(dāng)前調(diào)度器中,如果該調(diào)度器上沒有協(xié)程在運(yùn)行,會(huì)立刻 resume 這個(gè)協(xié)程
        if (custom_resume) {
            custom_resume(co);
        } else {
            coroutine_add(co);
        }
        return CHANNEL_ALT_SUCCESS;
    } else {
        // altqueue 里沒有任務(wù)
        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;
    }
}

這個(gè)函數(shù)代碼比較多,總結(jié)起來就是:

  1. 根據(jù) c->op 取出反操作隊(duì)列尾部的任務(wù)。
  2. 拿到該任務(wù)保存的協(xié)程對(duì)象。
  3. 如果是 SEND 操作,執(zhí)行綁定在 chan_alt 上的 custom_exec,這個(gè)函數(shù)主要是這句代碼 [self.buffList addObject:val ?: kCOChanNilObj],就是把 send 后面的參數(shù)添加到 COChanbuffList 屬性里。
  4. resume 第二步保存的協(xié)程對(duì)象。

到這里我們就可以知道,當(dāng)滿足 altcanexec 的條件之后:

  1. 如果調(diào)用 send_nonblock 函數(shù),那么會(huì)取出 RECEIVE 隊(duì)列中的任務(wù),把 send 過來的 val 放到 buffList 中,然后通過 custom_resume 恢復(fù) RECEIVE 任務(wù)中的協(xié)程,恢復(fù)之后會(huì)從 buffList 里面取出剛才 send 傳過來的 val,然后 return 出去。
  2. 如果調(diào)用 receive_nonblock 函數(shù),會(huì)取出 SEND 隊(duì)列中的任務(wù),把 send 過來的 val 放到 buffList 中,恢復(fù) RECEIVE 任務(wù)中的協(xié)程。執(zhí)行完 SEND 協(xié)程的代碼后繼續(xù)執(zhí)行 return CHANNEL_ALT_SUCCESS,返回到上層后 receive_nonblock 會(huì)返回 send 存在 buffList 中的值。

到此,整個(gè) await 的流程已經(jīng)比較清晰了,如下圖:


一次 await 時(shí)序.png

最后

筆者的這篇文章主要從一個(gè)簡單的協(xié)程例子開始,按著代碼執(zhí)行步驟一步一步帶大家分析整個(gè)協(xié)程執(zhí)行的流程,大家可以邊看文章邊跟著源碼過一遍加深記憶。整個(gè)協(xié)程實(shí)現(xiàn)異步的同步化表達(dá)的過程核心在COChan,也就是一個(gè)阻塞的消息隊(duì)列。當(dāng)然還有其它的一些類(比如COActor)沒有在這里展開講,其實(shí)原理都差不多,它們的核心都是基于協(xié)程的幾個(gè)族函數(shù)。
在下一篇文章我會(huì)繼續(xù)帶大家分析這幾個(gè)族函數(shù)在 ARM64 下的實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。