協(xié)程二三事(1)

1. 協(xié)程介紹

協(xié)程(coroutine)是近些年來在后臺開發(fā)方向比較火的一個概念瞬内,實際上秕衙,協(xié)程在歷史上比線程還要早些,而最近火起來則是因為近來后臺服務開發(fā)中遇到的C10K問題導致慷蠕。Wiki中給出的定義:

協(xié)程是一種程序組件掸冤,是由子例程(過程厘托、函數(shù)、例程稿湿、方法铅匹、子程序)的概念泛化而來的,子例程只有一個入口點且只返回一次饺藤,而協(xié)程允許多個入口點包斑,可以在指定位置掛起和恢復執(zhí)行。

單看定義涕俗,比較晦澀難懂罗丰。本質上,協(xié)程可以粗略理解為用戶態(tài)線程再姑,主要是用來解決在IO Bound型服務當中萌抵,如何更加有效地榨取CPU的使用效率。(同時元镀,還要兼顧程序開發(fā)的效率和可讀性绍填,因此異步回調的開發(fā)模式雖然在Nginx引領下大放異彩,但對程序開發(fā)人員來說并不友好)栖疑。用戶態(tài)線程有兩個問題必須被處理:

  • 碰到阻塞條件(或者其他觸發(fā)條件)讨永,進程必須能夠被掛起;
  • 由于沒有時鐘阻塞遇革,進程需要有自己進行線程調度的能力卿闹。
    因此揭糕,如果一種“用戶態(tài)線程”的實現(xiàn),使得每個線程可以通過自己調用某個方法(如yield等)锻霎,主動交出控制權插佛,那么我們就稱這種用戶態(tài)線程為協(xié)程(協(xié)作式線程)。
    一個例子就是生產者消費者模型量窘,如果在此處將線程換為協(xié)程雇寇,即一個協(xié)程負責生產產品并放入隊列,另一個負責把產品從隊列中取出并進行消費蚌铜,同時為了提高效率可以一次生產或消費多個產品锨侯,則偽代碼如下:
# producer coroutine
loop
while queue is not full
  create some new items
  add the items to queue
yield to consumer

# consumer coroutine
loop
while queue is not empty
  remove some items from queue
  use the items
yield to producer

2. 協(xié)程實現(xiàn)學習筆記

在C/C++中,函數(shù)調用通過壓棧的方式完成冬殃,具體過程如下:

  1. 第一個進棧的是主函數(shù)中函數(shù)調用后的下一條指令(函數(shù)調用語句的下一條可執(zhí)行語句)的地址囚痴;
  2. 然后是函數(shù)的各個參數(shù),在大多數(shù)的C編譯器中审葬,參數(shù)是由右往左入棧的深滚,然后是函數(shù)中的局部變量。注意靜態(tài)變量是不入棧的涣觉;
  3. 當本次函數(shù)調用結束后痴荐,局部變量先出棧,然后是參數(shù)官册,最后棧頂指針指向最開始存的地址生兆,也就是主函數(shù)中的下一條指令,程序由該點繼續(xù)運行膝宁。

在被調函數(shù)執(zhí)行完成前鸦难,主調函數(shù)無法獲取其上下文,因為函數(shù)變量已經壓棧暫時無法使用员淫。因此合蔽,實現(xiàn)協(xié)程本質就是如何讓C++實現(xiàn)yield的功能。

利用Linux提供的上下文保存機制介返,協(xié)程實現(xiàn)有ucontext系列和setjmp/longjmp系列拴事。這里先對ucontext系列進行簡單說明。ucontext是GNU C提供的一組用于創(chuàng)建映皆、保存挤聘、切換用戶態(tài)執(zhí)行上下文(context)的API轰枝,主要包括以下四個函數(shù):

void makecontext (ucontext_t *ucp, void (*func)(), int argc, ...);
int  swapcontext (ucontext_t *oucp, ucontext_t *ucp);
int  getcontext  (ucontext_t *ucp);
int  setcontext  (const ucontext_t *ucp);

typedef struct ucontext {
  struct ucontext *uc_link;
    sigset_t       uc_sigmask;
    stack_t         uc_stack;
    mcontext_t uc_mcontext;
    ...
} ucontext_t;

其中捅彻,

  1. sigset_t和stack_t定義在標準頭文件<signal.h>中,uc_link字段保存當前context執(zhí)行結束后執(zhí)行的下一個context記錄鞍陨;
  2. uc_sigmask記錄該context運行階段需要屏蔽的信號步淹;
  3. uc_stack是該context運行時的棧信息从隆,最后一個字段uc_mcontext保存具體的程序執(zhí)行上下文——PC值、堆棧指針缭裆、寄存器值等键闺,實現(xiàn)方式與平臺、硬件相關澈驼。

簡單介紹下四個函數(shù):

int makecontext(ucontext_t ucp, void (func)(), int argc, ...)

makecontext函數(shù)用來初始化一個ucontext_t類型結構辛燥,func指明該context的入口函數(shù),argc指明參數(shù)個數(shù)缝其,后續(xù)緊跟各個參數(shù)(每個參數(shù)都是int型)挎塌;
另外,在調用makecontext之前内边,一般還需要顯式的指明其初始棧信息(棧指針SP及棧大辛穸肌)和運行時的信號屏蔽掩碼(signal mask)。同時也可以指定uc_link字段漠其,這樣在func函數(shù)返回后嘴高,就會切換到uc_link指向的context繼續(xù)執(zhí)行。

int getcontext(ucontext_t *ucp)

getcontext用來將當前執(zhí)行狀態(tài)上下文保存到ucp指向的上下文結構當中和屎,若后續(xù)調用setcontext或者swapcontext恢復該上下文拴驮,則程序會沿著getcontext調用點之后繼續(xù)執(zhí)行,看起來好像剛從getcontext函數(shù)返回一樣柴信。這個功能和setjmp類似莹汤,都是保存執(zhí)行狀態(tài)以便后續(xù)能夠繼續(xù)執(zhí)行,但是:getcontext函數(shù)的返回值只能表示本次操作是否執(zhí)行正確颠印,而不能用來區(qū)分是直接從getcontext操作返回纲岭,還是由于setcontext/swapcontext恢復狀態(tài)導致的返回,這與setjmp是不一樣的线罕。

int setcontext(const ucontext_t *ucp)

setcontext用來將當前程序執(zhí)行切換到ucp所指向的上下文狀態(tài)止潮,在執(zhí)行正確的情況下,setcontext直接切入到新的執(zhí)行狀態(tài)钞楼,不會再返回喇闸。比如我們用上面介紹的makecontext初始化了一個新的上下文,并將入口指向某函數(shù)func()询件,那么setcontext成功后就會馬上運行func()函數(shù)燃乍。

int swapcontext(ucontext_t *oucp, ucontext_t *ucp)

swapcontext可以理解為以上兩個操作的組合:

  • 首先getcontext(oucp); //保存當前上下文到oucp
  • 然后setcontext(ucp); //執(zhí)行ucp上下文

理論上有上面3個函數(shù)可以滿足需要。但由于getcontext不能區(qū)分返回狀態(tài)宛琅,因此進行上下文切換時需要保存額外的信息來判斷刻蟹,比較麻煩。為了簡化實現(xiàn)嘿辟,swapcontext用來“原子”地完成舊狀態(tài)的保存和切換到新狀態(tài)舆瘪。(并非真正的原子操作片效,在多線程情況下也會引入一些調度方面的問題)

一個具體的例子:

#include <ucontext.h>
#include <stdio.h>

void func1(void* arg)
{
    puts("func1");
}

void func2(void* arg)
{
    puts("func2");
}

int main()
{
    char stack[1024*128];
    ucontext_t child,main;

    getcontext(&child); //獲取當前上下文
    child.uc_stack.ss_sp = stack;//指定棧空間
    child.uc_stack.ss_size = sizeof(stack);//指定椨⒐牛空間大小
    child.uc_stack.ss_flags = 0;
    child.uc_link = &main;//設置后繼上下文

    makecontext(&child,(void (*)(void))func1,0);//設置child協(xié)程執(zhí)行func1函數(shù)
    swapcontext(&main,&child);//保存當前上下文到main,執(zhí)行child上下文,因為child上下文后繼是main淀衣,所以執(zhí)行了func1函數(shù)后,會回到此處
    puts("back to main 1st");//如果設置了后繼上下文召调,func1函數(shù)指向完后會返回此處

    makecontext(&child,(void (*)(void))func2,0);
    swapcontext(&main,&child);
    puts("back to main 2nd");
    return 0;
}

//程序輸出:
//func1
//back to main 1st
//func2
//back to main 2nd

在云風實現(xiàn)的協(xié)程庫中膨桥,就是依據(jù)ucontext庫,抽象并實現(xiàn)了自己的協(xié)程調度Scheduler唠叛,在Scheduler中申請了單獨的椆欤空間,與進程執(zhí)行流的棽J空間進行交換保存保存介牙,具體可以參考這一段(來自云風的協(xié)程庫):

void  coroutine_resume(struct schedule * S, int id) {
    assert(S->running == -1);
    assert(id >=0 && id < S->cap);
    struct coroutine *C = S->co[id];
    if (C == NULL)
        return;
    int status = C->status;
    switch(status) {
    case COROUTINE_READY:
        getcontext(&C->ctx);
        C->ctx.uc_stack.ss_sp = S->stack;
        C->ctx.uc_stack.ss_size = STACK_SIZE;
        C->ctx.uc_link = &S->main;
        S->running = id;
        C->status = COROUTINE_RUNNING;
        uintptr_t ptr = (uintptr_t)S;
        makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
        swapcontext(&S->main, &C->ctx);
        break;
    case COROUTINE_SUSPEND:
        memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
        S->running = id;
        C->status = COROUTINE_RUNNING;
        swapcontext(&S->main, &C->ctx);
        break;
    default:
        assert(0);
    }
}

static void _save_stack(struct coroutine *C, char *top) {
    char dummy = 0;
    assert(top - &dummy <= STACK_SIZE);
    if (C->cap < top - &dummy) {
        free(C->stack);
        C->cap = top-&dummy;
        C->stack = malloc(C->cap);
    }
    C->size = top - &dummy;
    memcpy(C->stack, &dummy, C->size);
}

void coroutine_yield(struct schedule * S) {
    int id = S->running;
    assert(id >= 0);
    struct coroutine * C = S->co[id];
    assert((char *)&C > S->stack);
    _save_stack(C,S->stack + STACK_SIZE);
    C->status = COROUTINE_SUSPEND;
    S->running = -1;
    swapcontext(&C->ctx , &S->main);
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市澳厢,隨后出現(xiàn)的幾起案子环础,更是在濱河造成了極大的恐慌,老刑警劉巖剩拢,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件线得,死亡現(xiàn)場離奇詭異,居然都是意外死亡徐伐,警方通過查閱死者的電腦和手機贯钩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來办素,“玉大人角雷,你說我怎么就攤上這事⌒源” “怎么了勺三?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長需曾。 經常有香客問我吗坚,道長,這世上最難降的妖魔是什么呆万? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任商源,我火速辦了婚禮,結果婚禮上谋减,老公的妹妹穿的比我還像新娘牡彻。我一直安慰自己,他們只是感情好逃顶,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布讨便。 她就那樣靜靜地躺著,像睡著了一般以政。 火紅的嫁衣襯著肌膚如雪霸褒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天盈蛮,我揣著相機與錄音废菱,去河邊找鬼。 笑死抖誉,一個胖子當著我的面吹牛殊轴,可吹牛的內容都是我干的。 我是一名探鬼主播袒炉,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼旁理,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了我磁?” 一聲冷哼從身側響起孽文,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎夺艰,沒想到半個月后芋哭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡郁副,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年减牺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片存谎。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡拔疚,死狀恐怖,靈堂內的尸體忽然破棺而出既荚,到底是詐尸還是另有隱情草雕,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布固以,位于F島的核電站墩虹,受9級特大地震影響,放射性物質發(fā)生泄漏憨琳。R本人自食惡果不足惜诫钓,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望篙螟。 院中可真熱鬧菌湃,春花似錦、人聲如沸遍略。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至下愈,卻和暖如春纽绍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背势似。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工拌夏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人履因。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓障簿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親栅迄。 傳聞我的和親對象是個殘疾皇子站故,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容

  • 原文鏈接:https://github.com/EasyKotlin 在常用的并發(fā)模型中,多進程毅舆、多線程世蔗、分布式是...
    JackChen1024閱讀 10,744評論 3 23
  • 協(xié)程(Coroutine)是目前比較流行的一種并發(fā)編程模型,在主流的編程語言里都能找到協(xié)程的實現(xiàn)朗兵,比如libtas...
    FunFeast閱讀 610評論 0 2
  • 從三月份找實習到現(xiàn)在污淋,面了一些公司,掛了不少余掖,但最終還是拿到小米寸爆、百度、阿里盐欺、京東赁豆、新浪、CVTE冗美、樂視家的研發(fā)崗...
    時芥藍閱讀 42,277評論 11 349
  • 協(xié)程 在常用的并發(fā)模型中多線程粉洼、多進程节预、分布式是最普遍的,不過近些年來逐漸有一些語言以first-class或者l...
    點融黑幫閱讀 4,848評論 1 17
  • 參考文章: Python 中的進程属韧、線程安拟、協(xié)程、同步宵喂、異步糠赦、回調 簡明網絡I/O模型---同步異步阻塞非阻塞之惑...
    _heqin閱讀 2,125評論 0 2