協程庫libco學習使用入門示例

簡介

libco是微信後臺大規模使用的c/c++協程庫,2013年至今穩定運行在微信後臺的數萬臺機器上。

libco通過僅有的幾個函數接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者異步的寫法,如線程庫一樣輕鬆。同時庫裏面提供了socket族函數的hook,使得後臺邏輯服務幾乎不用修改邏輯代碼就可以完成異步化改造。

libco是一個源碼簡潔而且性能高效的協程庫,可以通過閱讀源碼學習理解協程的概念及技術實現方案,也可根據項目需要使用協程方案,本章節試圖做一個入門示例,從源碼編譯到項目使用,力爭記錄libco的入門使用方法。

說明:因爲平臺的問題,可能實際操作中遇到的問題與本文章記錄的不一樣,請自行分析解決,不可完全照搬

libco庫下載編譯

從Github下下載並編譯libco,編譯很簡單,直接make則可以,如下:

tony:~/code/github$ git clone https://github.com/Tencent/libco.git
Cloning into 'libco'...
remote: Enumerating objects: 256, done.
remote: Total 256 (delta 0), reused 0 (delta 0), pack-reused 256
Receiving objects: 100% (256/256), 180.23 KiB | 151.00 KiB/s, done.
Resolving deltas: 100% (143/143), done.
tony:~/code/github$ cd libco/
tony:~/code/github/libco$ make
xxx編譯過程xxx
tony:~/code/github/libco$ ls lib/*
lib/libcolib.a
tony:~/code/github/libco$ ls solib/*
solib/libcolib.so

編譯完成後,可以看到生成了對應的靜態庫文件lib/libcolib.a和動態庫文件solib/libcolib.so,後續項目編碼使用時,只需要包含頭文件co_routine.h即可。

新建Linux控制檯項目測試libco

在多線程編程教程中,有一個經典的例子:生產者消費者問題。事實上,生產者消費者問題也是最適合協程的應用場景,因此我們這次使用生產者消費者場景來測試libco。
因爲使用靜態庫文件開發方便,因此本次使用靜態庫來開發測試,首先需要將唯一引用的頭文件引入到項目中,可直接拷貝到項目目錄並添加到項目文件中,並根據libco開源項目及示例說明,學習使用方案。

1.創建協程對象

// 聲明一個協程對象類型指針
stCoRoutine_t* pProducerCo = NULL;

// 調用函數創建協程對象,函數內會分配對象指針
// 看源碼可知該函數必返回0,因此不必判斷返回值
co_create(&pProducerCo, NULL, Producer, &p);

函數原型聲明爲:

int co_create( stCoRoutine_t **co,const stCoRoutineAttr_t *attr,void *(*routine)(void*),void *arg )

參數stCoRoutine_t爲出參,返回創建的協程對象
參數stCoRoutineAttr_t爲入參,指定創建協程的屬性,本次使用默認屬性,傳空
參數routine爲入參,指定協程執行函數
參數arg爲入參,指定協程執行函數的參數

2.創建生產者和消費者協程執行函數

void* Producer(void* arg);
void* Consumer(void* arg);

根據co_create函數聲明的協程執行函數原型,分別創建生產者和消費者的函數,需要注意的是,協程函數體內需要先啓用協程HOOK,如下所示:

void* Producer(void* arg)
{
    // 啓用協程HOOK項
    co_enable_hook_sys();
    stPara_t* p = (stPara_t*)arg;
    while (true)
    {
    }
    return NULL;
}

3.指定協程執行參數
生產者消費者之間的通信需要使用條件變量,且需要有個公共池用於存取數據,因此可聲明協程參數爲:

struct stPara_t
{
    // 條件變量
    stCoCond_t* cond;
    // 數據池
    std::vector<int> vecData;
    // 數據ID
    int id;
    // 協程id
    int cid;
};

在創建協程前,創建該參數對象,並初始化:

stPara_t p;
p.cond = co_cond_alloc();
p.cid = p.id = 0;

4.啓動協程
使用co_create創建的協程並未啓用爲執行,需要我們使用co_resume顯示啓動協程,函數co_resume聲明原型如下:

void co_resume(stCoRoutine_t *co)

參數stCoRoutine_t爲入參,是co_create的出參,該函數用於首次啓動協程或者使得當前協程讓出執行權限給其他協程,因此是resume而不是start

5.啓動協程事件處理循環
協程創建啓動完之後,我們需要執行epoll的事件循環處理,協助協程的調度及異步操作,代碼如下:

// 啓動循環事件
co_eventloop(co_get_epoll_ct(), NULL, NULL);

最後我們可以編寫以下測試代碼:

#include <time.h>
#include <stdlib.h>
#include <iostream>
#include <vector>
#include <string>
#include "co_routine.h"

void* Producer(void* arg);
void* Consumer(void* arg);

struct stPara_t
{
    // 條件變量
    stCoCond_t* cond;
    // 數據池
    std::vector<int> vecData;
    // 數據ID
    int id;
    // 協程id
    int cid;
};

int main()
{
    stPara_t p;
    p.cond = co_cond_alloc();
    p.cid = p.id = 0;

    srand(time(NULL));
    // 協程對象(CCB),一個生產者,多個消費者
    const int nConsumer = 2;
    stCoRoutine_t* pProducerCo = NULL;
    stCoRoutine_t* pConsumerCo[nConsumer] = { NULL };

    // 創建啓動生產者協程
    // 看源碼可知該函數必返回0
    co_create(&pProducerCo, NULL, Producer, &p);
    co_resume(pProducerCo);
    std::cout << "start producer coroutine success" << std::endl;

    // 創建啓動消費者協程
    for (int i = 0; i < nConsumer; i++)
    {
        co_create(&pConsumerCo[i], NULL, Consumer, &p);
        co_resume(pConsumerCo[i]);
    }
    std::cout << "start consumer coroutine success" << std::endl;

    // 啓動循環事件
    co_eventloop(co_get_epoll_ct(), NULL, NULL);

    return 0;
}


void* Producer(void* arg)
{
    // 啓用協程HOOK項
    co_enable_hook_sys();

    stPara_t* p = (stPara_t*)arg;
    int cid = ++p->cid;
    while (true)
    {
        // 產生隨機個數據
        for (int i = rand() % 5 + 1; i > 0; i--)
        {
            p->vecData.push_back(++p->id);
            std::cout << "[" << cid << "] + add new data:" << p->id << std::endl;
        }
        // 通知消費者
        co_cond_signal(p->cond);
        // 必須使用poll等待
        poll(NULL, 0, 1000);
    }
    return NULL;
}
void* Consumer(void* arg)
{
    // 啓用協程HOOK項
    co_enable_hook_sys();

    stPara_t* p = (stPara_t*)arg;
    int cid = ++p->cid;
    while (true)
    {
        // 檢查數據池,無數據則等待通知
        if (p->vecData.empty())
        {
            co_cond_timedwait(p->cond, -1);
            continue;
        }
        // 消費數據
        std::cout << "[" << cid << "] - del data:" << p->vecData.front() << std::endl;
        p->vecData.erase(p->vecData.begin());
    }
    return NULL;
}

編譯鏈接

因爲項目引用靜態庫,因此需要引入libcolib.a文件,引入該文件分爲兩步:1.指定庫文件路徑,2.指定引用庫文件名。在VS中具體操作如下:

1.配置靜態庫目錄

指定庫文件的目錄,接着再指定庫文件名:

2.配置靜態庫文件

點擊確定後,可以試着編譯項目,發現編譯出錯,主要信息如下:

1>D:\AppData\PerDoc\temp\demo\libco_demo\libcolib.a(co_hook_sys_call.o) : error : In function `__static_initialization_and_destruction_0(int, int) [clone .constprop.28]':
1>/home/tony/code/github/libco/co_hook_sys_call.cpp(107): error : undefined reference to `dlsym'
1>D:\AppData\PerDoc\temp\demo\libco_demo\libcolib.a(co_hook_sys_call.o) : error : /home/tony/code/github/libco/co_hook_sys_call.cpp:109: more undefined references to `dlsym' follow
1>D:\AppData\PerDoc\temp\demo\libco_demo\libcolib.a(co_routine.o) : error : In function `co_getspecific(unsigned int)':
1>/home/tony/code/github/libco/co_routine.cpp(1062): error : undefined reference to `pthread_getspecific'

上述錯誤主要爲【undefined reference to ‘dlsym’】和【undefined reference to ‘pthread_getspecific’】,此爲平臺類的庫鏈接錯誤,需要調整編譯選項,需要在鏈接時添加額外選項-pthread -Wl,--no-as-needed -ldl,配置如下:

鏈接選項配置

此時再次重新編譯鏈接,則可以發現成功。

調試運行

此時按F5調試運行,則可以看到程序可以正常執行,結果如下:

調試運行結果

通過運行結果發現:

  1. 協程庫有效,我們未通過多線程技術,實現了生產者消費者運行場景,且生產後立即消費,無延時,性能佳
  2. 測試代碼中有一個生產者,兩個消費者,但是在運行過程中,每個批次都是其中一個消費者在工作,但是不同批次則會在兩個消費者中交替選取。分析代碼可發現,因爲libco的協程調度是由程序控制,而我們的代碼中當某個消費者協程開始消費後,則會一直消費直到沒有數據纔會讓出協程,因此每次消費者都會將數據消費完纔將協程讓出給生產者
  3. 因爲協程運行在同一個線程內,所以在同一時刻一個線程內的多個協程只會有一個協程在執行,因此本示例中生產者和消費者都未對數據加鎖訪問,但仍然是安全的

項目地址

項目github地址

參考文檔

騰訊開源協程庫libco-原理及應用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章