libco協程庫源碼解讀

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

????協程,又被稱為用戶級線程,是在應用層被調度,可以減少因為調用系統調用而阻塞的線程切換的時間.目前有很多協程的實現,由于微信內部大量使用了其直研的的libco協程庫,所以我選擇了騰訊開源的libco協程庫進行研究,學習協程的基本思想.

1,基本原理

? ? 協程實質上可以看成是子程序、函數。一個線程上面可以運行多個協程,但是同一時間只能運行一個協程,協程在線程上的切換,是由于遇到阻塞的操作,或者主動讓出線程使用權。比如,有10個協程,當前線程正在運行協程1,然后協程1執行一個recv的阻塞操作,協程的調度器能夠檢測到這個操作,將協程1切換出去,將協程2調度進來執行。如果沒有協程的調度器,此時協程1將會由于調用recv這個系統調用且數據未到達而阻塞,進行休眠,此時操作系統將會發生線程切換,調度其他線程執行,而線程切換非常耗時,高達幾十微秒(同事測試是20us),即便新執行的線程是用戶任務相關的,用戶任務也會多了幾十微秒的線程切換的消耗。而如果使用協程,協程之間的切換只需要幾百納秒(同事測試為0.35us,即350納秒),耗時很少。這就是協程發揮優勢的地方。

? ? 下面講解libco的源碼部分,有一篇文章:C++開源協程庫libco-原理與應用.pdf,非常深入的講解了libco的原理,而且不枯燥,十分推薦讀者先看看這篇文章。

? ? 由于libco是非對稱的協程機制,如果從當前協程A切換到協程B,而協程B又沒有切換到下一個協程,在協程B執行結束之后,會返回到協程A執行。

2,libco基本框架

? ? libco中的基本框架如下(引自C/C++協程庫libco:微信怎樣漂亮地完成異步化改造):

18084202_GyCc.png

協程接口層實現了協程的基本源語。co_create、co_resume等簡單接口負責協程創建于恢復。co_cond_signal類接口可以在協程間創建一個協程信號量,可用于協程間的同步通信。

系統函數Hook層負責主要負責系統中同步API到異步執行的轉換。對于常用的同步網絡接口,Hook層會把本次網絡請求注冊為異步事件,然后等待事件驅動層的喚醒執行。

事件驅動層實現了一個簡單高效的異步網路框架,里面包含了異步網絡框架所需要的事件與超時回調。對于來源于同步系統函數Hook層的請求,事件注冊與回調實質上是協程的讓出與恢復執行。

本文通過講解接口層的幾個主要函數,使讀者對libco協程的框架和原理有一個大概的認識,下一篇文章將會講解libco如何處理事件循環等。

下面我們從幾個主要的協程函數一一分析。

3,主要函數源碼解析

  • co_create?????首先來開一下協程創建的函數,源碼如下:
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{if( !co_get_curr_thread_env() ) {co_init_curr_thread_env();}stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg );*ppco = co;return 0;
}
void co_init_curr_thread_env()
{pid_t pid = GetPid();	g_arrCoEnvPerThread[ pid ] = (stCoRoutineEnv_t*)calloc( 1,sizeof(stCoRoutineEnv_t) );stCoRoutineEnv_t *env = g_arrCoEnvPerThread[ pid ];env->iCallStackSize = 0;struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );self->cIsMain = 1;env->pending_co = NULL;env->occupy_co = NULL;coctx_init( &self->ctx );env->pCallStack[ env->iCallStackSize++ ] = self;stCoEpoll_t *ev = AllocEpoll();SetEpoll( env,ev );
}

????? ? co_create()的第一行判斷是當前線程初始化環境變量的判斷,如果沒進行環境初始化,那么調用co_init_curr_thread_env() 進行環境初始化,會生成當前環境g_arrCoEnvPerThread[ GetPid() ]的第一個協程 env->pCallStack,其?cIsMain 標志位 1,iCallStackSize表示協程層數,目前只有1層,AllocEpoll()函數中初始化當前環境env的 pstActiveList,pstTimeoutList 這兩個列表,這兩個列表分別記錄了活動協程和超時協程。環境初始化操作在一個線程中只會進行一次。在初始化完成之后,會調用co_create_env()創建一個新的協程,新協程的結構體中的env這個域始終指向當前協程環境g_arrCoEnvPerThread[ GetPid() ]。新協程創建之后,并沒有做什么操作。

  • co_resume
    void co_resume( stCoRoutine_t *co )
    {stCoRoutineEnv_t *env = co->env;stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];if( !co->cStart ){coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );co->cStart = 1;}env->pCallStack[ env->iCallStackSize++ ] = co;co_swap( lpCurrRoutine, co );
    }
    co_resume()函數是切換協程的函數,也可以稱為是啟動協程的函數。co_resume()函數的第一行是獲取當前線程的協程環境env,第二行獲取當前正在執行的協程,也即馬上要被切換出去的協程。接下來判斷待切換的協程co是否已經被切換過,如果沒有,那么為co準備上下文,cStart字段設置為1。這里為co準備的上下文,就是在coctx_make()函數里面,這個函數將函數指針CoRoutineFunc賦值給co->ctx的reg[0],將來上下文切換的時候,就能切換到reg[0]所指向的地址去執行.準備好co的上下文之后,然后將待切換的協程co入棧,置于協程環境env的協程棧的頂端,表明當前最新的協程是co。注意,這并不是說協程棧中只有棧頂才是co,可能棧中某些位置也存了co。最后,調用co_swap(),該函數將協程上下文環境切換為co的上下文環境,并進入co指定的函數內執行,之前被切換出去的協程被掛起,直到co主動yield,讓出cpu,才會恢復被切換出去的協程執行.注意,這里的所有的協程都是在當前協程執行的,也就是說,所有的協程都是串行執行的,調用co_resume()之后,執行上下文就跳到co的代碼空間中去了。因為co_swap()要等co主動讓出cpu才會返回,而co的協程內部可能會resume新的協程繼續執行下去,所以co_swap()函數調用可能要等到很長時間才能返回。
    void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
    {stCoRoutineEnv_t* env = co_get_curr_thread_env();//get curr stack spchar c;curr->stack_sp= &c;if (!pending_co->cIsShareStack){env->pending_co = NULL;env->occupy_co = NULL;}else {env->pending_co = pending_co;//get last occupy co on the same stack memstCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;//set pending co to occupy thest stack mem;pending_co->stack_mem->occupy_co = pending_co;env->occupy_co = occupy_co;if (occupy_co && occupy_co != pending_co){save_stack_buffer(occupy_co);}}//swap contextcoctx_swap(&(curr->ctx),&(pending_co->ctx) );//stack buffer may be overwrite, so get again;stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();stCoRoutine_t* update_occupy_co =  curr_env->occupy_co;stCoRoutine_t* update_pending_co = curr_env->pending_co;if (update_occupy_co && update_pending_co && update_occupy_co != update_pending_co){//resume stack bufferif (update_pending_co->save_buffer && update_pending_co->save_size > 0){memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer, update_pending_co->save_size);}}
    }
    在co_swap()函數代碼中,由于libco不是共享棧的模式,即pending_co->cIsShareStack為0,所以執行了if分支,接下來執行coctx_swap(),這是一段匯編源碼,內容就是從curr的上下文跳轉到pending_co的上下文中執行,通過回調CoRoutineFunc()函數實現,此時當前線程的cpu已經開始執行pending_co協程中的代碼,直到pending_co主動讓出cpu,才接著執行coctx_swap()下面的代碼,由于update_occupy_co為NULL,下面的if語句沒有執行,所以相當于coctx_swap()下面沒有代碼,直接返回到curr協程中.
  • co_yield
    co_yield()與co_yield_ct()的功能是一樣的,都是使得當前協程讓出cpu.
    void co_yield_env( stCoRoutineEnv_t *env )
    {stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];env->iCallStackSize--;co_swap( curr, last);
    }
    co_yield_env()函數中的第二行獲取當前執行的協程,也即當前協程環境的協程棧的棧頂,函數的第一行獲取協程棧的次頂,也即上一次被切換的協程last,從這里也可以看出,libco的協程讓出cpu,只能讓給上一次被切換出去的協程.最后一行是co_swap()函數,前面講到,該函數會進入last協程的上下文去執行代碼,也就是回到上次co_resume()函數內部的co_swap()的地方,繼續往下走.
    當協程正常結束的時候,會繼續執行CoRoutineFunc()函數,將協程的cEnd設置為1,表示已經結束,并執行一次co_yield_env(),讓出cpu,切換回上一次被讓出的協程繼續執行.
    這里有一點我之前不太理解,懷疑會發生棧溢出的地方,那就是在調用co_yield_env(),進入co_swap()之后,調用coctx_swap(),切換到上一次的last協程的上下文,那么當前協程的co_swap()函數里面的變量,都是在棧空間上面的,切換到last協程的上下文之后,那些變量依然在棧空間上面,不會被銷毀,直到回到了main函數的協程,還是沒有被銷毀。其實這是個誤區,這些變量其實不是在棧空間上面,而是在CPU的通用寄存器里面,當調用coctx_swap()之后,這些寄存器變量就會保存到當前協程的棧空間中去,其實是我們之前co_create()函數malloc出來的一片堆空間。這是因為cpu的工作寄存器數量較多,而局部變量較少,而co_swap()函數的變量都是局部變量,直接存放在cpu的工作寄存器中,而coctx_swap()的作用就是將CPU的各個通用寄存器保存到coctx_t結構的regs[1] ~ regs[6]的位置,然后將last協程的coctx_t結構的regs[1]~regs[6]的內容加載到當前的通用寄存器中,并將執行cpu的執行順序切換到last協程中去執行。
  • co_release
    co_release()的功能比較簡單,就是釋放資源
    void co_release( stCoRoutine_t *co )
    {if( co->cEnd ){free( co );}
    }
  • co_self
    co_self()函數是獲取當前正在執行的協程,只要獲取到當前協程環境的線程棧頂的協程即可。
    stCoRoutine_t *co_self()
    {return GetCurrThreadCo();
    }
    stCoRoutine_t *GetCurrThreadCo( )
    {stCoRoutineEnv_t *env = co_get_curr_thread_env();if( !env ) return 0;return GetCurrCo(env);
    }
    stCoRoutine_t *GetCurrCo( stCoRoutineEnv_t *env )
    {return env->pCallStack[ env->iCallStackSize - 1 ];
    }
  • co_enable_hook_sys
    libco封裝了系統調用,在系統調用,比如send/recv/condition_wait等函數前面加了一層hook,有了這層hook就可以在系統調用的時候不讓線程阻塞而產生線程切換,co_enable_hook_sys()函數允許協程hook,當然也可以不允許hook,直接使用原生的系統調用。
    void co_enable_hook_sys()
    {stCoRoutine_t *co = GetCurrThreadCo();if( co ){co->cEnableSysHook = 1;}
    }

?

轉載于:https://my.oschina.net/u/2447371/blog/1591005

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/284903.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/284903.shtml
英文地址,請注明出處:http://en.pswp.cn/news/284903.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【ArcGIS風暴】如何將矢量數據(點、線、面)折點坐標轉為GeoJSON格式?

本文以案例的形式,講述在ArcGIS和QGIS專業軟件中,將矢量數據轉為GeoJSON的方法。 擴展閱讀:【GIS風暴】GeoJSON數據格式案例全解 文章目錄 一、ArcGIS將矢量數據轉為GeoJSON二、QGIS將矢量數據轉為GeoJSON一、ArcGIS將矢量數據轉為GeoJSON ArcGIS中提供的【要素轉JSON】工具…

TypeScript 3.9 正式發布!平均編譯時長從 26 秒縮短至 10 秒

作者 | 微軟官方博客 譯者 | 核子可樂 策劃 | 小智 稿源 | 前端之巔 今天,微軟在其官方博客宣布:TypeScript 3.9 版本已經正式發布,詳情見下文。 有些朋友可能對 TypeScript 還不太熟悉,這是一種以 JavaScript 為基礎開發的語…

(二)Harbor WEB的使用

接上一篇《安裝Harbor》,安裝好之后,接下來我們就進行Harbor web界面的操作吧! 轉載請標明出處:http://www.cnblogs.com/huangjc/p/6270405.html 瀏覽器登陸Harbor(默認用戶密碼:admin/Harbor12345&#x…

iVX低代碼平臺系列制作簡單的登錄界面

一、前言 iVX是啥,不理解的小伙伴可以猛戳這里 ----------------------點我 二、iVX平臺和現有編程語言的對比 三、iVX平臺和現有編程語言的對比 1、快速學習(周期短) iVX邏輯上相對是比較簡單的 所以初學者的話只需要從邏輯和具體功…

【CASS精品教程】CASS9.1等高線的繪制完整案例教程

在地形圖中,等高線是表示地貌起伏的一種最重要的手段。在CASS成圖時,可自動生成精度高的等高線,本文講解CASS9.1生成等高線的完整操作流程。 文章目錄 1. 展高程點2. 建立數字地面模型3. 修改數字地面模型4. 繪制等高線5. 等高線的修飾6. 繪制三維模型擴展閱讀: ArcGIS實驗…

Process.Start 為什么會引發“系統找不到指定的文件”異常

前言偶然發現,如果想用如下代碼在 .NET 6 中打開指定 URL:Process.Start("https://baidu.com");會引發異常:而同樣的代碼在 .NET Framework 中是可以正常執行的。難道,.NET 6 下的實現邏輯不一樣?深入探究通…

JVM 類型的生命周期學習

Java虛擬機通過裝載、連接和初始化一個JAVA類型,使該類型可以被正在運行的JAVA程序所使用,其中,裝載就是把二進制形式的JAVA類型讀入JAVA虛擬機中;而連接就是把這種讀入虛擬機的二進制形式的類型數據合并到虛擬機的運行時狀態中去…

js對象數組中的某屬性值 拼接成字符串

var arr[{id: "600", pId: null, name: "圖形的變化"},{id: "630", pId: "600", name: "投影與視圖"},{id: "631", pId: "630", name: "投影"},{id: "632", pId: "630",…

898A. Rounding#數的舍入

題目出處&#xff1a;http://codeforces.com/problemset/problem/898/A 題目大意&#xff1a;找一個數最近的整十的數 #include<iostream> using namespace std; int main(){int a,b;cin>>a;ba;while(1){if(a%100){cout<<a<<endl;return 0;}if(b%100){…

開店星簡直就是國內優秀的開源商城系統天花板

一、場景 1、大學生畢業設計做商城系統背景 好家伙、又到開學季節了&#xff0c;師妹讓我幫忙給指導大四的項目&#xff0c;作為畢業設計和為后面找工作積累項目經驗&#xff0c;要搞一個買賣二手閑置品的商城小程序和PC端商城、希望能夠快速學習、接入、修改部分功能&#xff…

【CASS精品教程】CASS9.1土方量的計算方法匯總

CASS9.1中,計算土方量的方法有:DTM法土方計算、斷面法進行土方量計算、方格網法土方計算、等高線法土方計算、區域土方量平衡等。本文以案例的形式,詳細講解土方量的計算過程。 文章目錄 一、DTM法土方計算二、斷面法進行土方量計算三、方格網法土方計算四、等高線法土方計算…

html標簽缺省(自帶)樣式大全

html標簽默認樣式整理 作者&#xff1a;佚名 來源&#xff1a;互聯網 時間&#xff1a;07-30 16:54:48 文為大家整理了html標簽默認樣式屬性及瀏覽器默認樣式等等&#xff0c;喜歡css布局的朋友們可以學下&#xff0c;希望對大家有所幫助html, address,blockquote,body, dd, …

VS2019 禁止Web項目停止調試后自動關閉瀏覽器(在瀏覽器窗口關閉時停止調試程序,在調試停止時關閉瀏覽器)

很多文章都說要修改以下兩處與“編輯并繼續”有關的選項&#xff1a; “編輯并繼續”是一種省時的功能&#xff0c;使你能夠在程序處于中斷模式時更改源代碼。 通過選擇執行命令&#xff08;如 "繼續" 或 "單步執行"&#xff09;繼續執行程序時&#xff0c…

iOS - block變量捕獲原理

block對變量的捕獲 1&#xff1a;可以捕獲不可以修改變量 局部變量2&#xff1a;可以捕獲且可以修改變量 全局變量靜態變量__block修飾的局部變量原理分析&#xff1a; 1. 局部變量為什么可以被捕獲確不能修改 int a 10; void (^blcok)() [^{NSLog("%d",a); } copy…

Shell 更好看的回顯

#!/bin/shsource /etc/init.d/functionsaction "hello" /bin/true轉載于:https://blog.51cto.com/itech/1768218

【ArcGIS風暴】ArcGIS中等高線高程標注/注記(打斷/消隱)方法案例匯總

本文以案例的形式,圖文并茂詳細講解在ArcGIS 10.6中,等高線高程標注、注記的方法。 文章目錄 一、屬性標注二、Maplex工具標注1. 使用Maplex標注引擎2. 標注轉換為注記3. 要素輪廓線掩膜4. 使用掩膜選項進行繪制參考閱讀: 【CASS精品教程】CASS9.1等高線的繪制完整案例教程 …

Blazor University (35)表單 —— 編寫自定義驗證

原文鏈接&#xff1a;https://blazor-university.com/forms/writing-custom-validation/編寫自定義驗證源代碼[1]請注意&#xff0c;與有關 EditContext、FieldIdentifiers 和 FieldState[2] 的部分一樣&#xff0c;這是一個高級主題。如前所述&#xff0c;FieldState 類保存表…

HTML 元素內部添加預加載

CSS&#xff1a; /*元素內部加載loading*/.innerLoading {height: 100%;width: 100%;display: flex;justify-content: center;align-items: center;}.innerLoading * {text-align: center;color: #737782cc;fill: #73777A;font-size: 1em !important;font-family: SimSun,SimHe…

Windows下怎樣安裝Tomcat

Tomcat 是開源的WEB應用容器&#xff0c;所以受到各位程序員和公司的親賴。在這里給大家介紹一下如何在Windows環境下安裝Tomcat綠色版本&#xff0c;希望能夠對大家有幫助。 1.首先去Tomcat官網下載Tomcat軟件&#xff0c;在百度中搜索Tomcat,進入英文網址http://tomcat.apach…

智能識別云服務端平臺之神【合合信息TextIn】

一、前言 眾所周知&#xff0c;隨著互聯網和人工智能的發展&#xff0c;我們非常多的場景需要用到智能“識別”功能&#xff0c;比如人臉識別、通用文字識別、表格識別、辦公文檔識別、身份證、名片、營業執照等國內外卡證文字識別等等&#xff0c;同時識別與理解面臨的全球性技…