Android NDK——初識協程(Coroutine)和libco的簡單介紹使用

CrazyMo_發表於2020-10-04

引言

協程不是程式或執行緒,其執行過程更類似於子例程,或者說不帶返回值的函式呼叫。
一個程式可以包含多個協程,可以對比與一個程式包含多個執行緒,因而下面我們來比較協程和執行緒。我們知道多個執行緒相對獨立,有自己的上下文,切換受系統控制;而協程也相對獨立,有自己的上下文,但是其切換由自己控制,由當前協程切換到其他協程由當前協程來控制。

一、協程Coroutine概述

協程(Coroutine)又名微執行緒,是一種很早就提出了的程式機制概念,只不過最近幾年才在一些程式語言(Lua、Kotlin )上得到廣泛應用等。

在這裡插入圖片描述

在協程概念出來之前,在所有的程式語中子程式(又稱之為函式或方法)都是基於棧實現層級呼叫的,函式呼叫有且只有一個入口和一個返回,線上程內部,呼叫順序是明確的自上而下。比如在一個執行緒內執行函式A,函式A內呼叫函式B,函式B內又呼叫函式C,那麼執行順序一定是首先執行A,當執行到B 時,進入函式B 內部,再當執行到C時,執行C,待C返回之後到B內部繼續往下執行,然後B返回到A繼續往下執行,也就是說傳統的函式呼叫機制中,要想從當前函式呼叫調到另一個的函式呼叫只能是主動去執行函式呼叫。而引入協程概念之後,函式執行過程中,可以產生類似於CPU中斷機制,去執行其他函式並且還可以在適當時候返回繼續執行

在這裡插入圖片描述

如上圖所示,可以通過yield()函式進行切換,FunctionA執行完Foo()後,執行FunctionB的Foo(),然後再返回來執行FunctionA的Bar(),最後執行FunctionB的Bar()。

二、協程的優勢

  • 協程具有極高的執行效率,因為子程式切換不涉及到執行緒的切換,而是由程式自身自主進行控制。避免了執行緒切換帶來的資源消耗。執行緒越多,協程效能優勢越明顯。
  • 協程不需要考慮執行緒的鎖機制,因為只有一個執行緒,自然也不存在同時寫變數時的資料安全隱患,在協程中不通過鎖來控制共享資源,而是通過狀態判定。

三、C語言主流的協程庫簡介

C語言主流的協程庫有libtask、libmill、boost、libgo、libco等。

四、協程的切換

  • 使用ucontext 系列介面,例如libtask。
  • 使用boost.context,純彙編實現,內部實現機制跟ucontext 完全不同,效率非常高,tbox也基於此。
  • 使用setjmp/longjmp 介面,例如libmill。

五、libco

1、libco概述

在這裡插入圖片描述

libco 基於效能考慮沒有使用ucontext 系列介面,而是自行編寫了一套匯編來處理上下文的切換,具體實現在coctx_swap.s裡。libco 在進行上下文切換時只儲存和交換了兩類東西:暫存器(函式引數暫存器、函式返回值暫存器、資料儲存類暫存器等)和棧(rsp棧頂指標)。相較於ucontext,缺少了浮點數上下文(因為在服務端程式設計幾乎用不到浮點數計算,此外libco的上下文切換隻支援x86 架構)、sigmask(訊號遮蔽掩碼,而取消sigmask 是因它會引發一次syscall,在效能上有所損耗)。

x86 架構下libco的效能約是ucontext 的 3.6倍。

libco 犧牲了通用性,把服務端環境下不需要的暫存器拷貝去掉,並對程式碼進行了極致優化,換取了高效的效能。libco框架圖如下:

在這裡插入圖片描述

底層基於i/o多路複用模型實現非同步i/o,中間層這是對系統函式的hook,主要是將阻塞的系統呼叫(如read、write)改為非同步呼叫,最上層是使用者介面層,直接提供對應的api,實現了協程原語(協程建立、執行、排程等)並且實現了一套協程間的通訊的訊號量。核心思想有兩點:

  • 對協程上下文的切換
  • 對同步介面進行非同步化轉換

2、libco 的使用說明

2.1、co_create函式建立並初始化協程物件

2.2、宣告一個協程物件型別的指標

stCoRoutine_t* pointer_produce=NULL;

2.3、呼叫co_create函式建立初始化協程物件

//函式原型宣告
int co_create(stCoRoutine_t  **co,const stCoRoutineAttr_t  *attr, void *(routine)(void *),void *arg  );

//函式呼叫
co_create(&pointer_produce,NULL,Producer,&params);

co_create的函式返回值恆為0,四個引數分別代表:

  • stCoRoutine_t **co——出參,二級指標,返回建立的協程物件的地址
  • const stCoRoutineAttr_t *attr——入參,用於指定建立協程時需要指定的屬性時,使用預設引數則傳NULL
  • void *(routine)(void *)——函式指標,協程執行的函式
  • void *arg——代表協程執行函式時所需要的引數

2.4、coctx_swap進行協程上下文切換

//函式原型
extern void coctx_swap(coctx_t *,coctx_t *)asm("coctx_swap");

coctx_swap 本質上是呼叫匯編函式,該函式下有兩個型別為costx_t 的引數,分別代表掛起恢復的協程。以下是coctx_t 結構體的實現:

struct coctx_t
{
	void* regs[14];
	size_t ss_size;
	char* ss_sp; 
};

本質上所謂協程上下文切換就是做了三件事:

  • 儲存掛起協程的暫存器資訊
  • 恢復啟動協程的暫存器資訊
  • 跳轉到啟動協程的掛起地址繼續執行

在這裡插入圖片描述

一般不需要我們開發者直接呼叫

3、共享棧和私有棧

協程是使用者級執行緒,其共享同一套暫存器,當需要掛起該協程時,需要把其對應的暫存器資訊儲存起來,regs 就是用於儲存暫存器資訊的;而ss_sp 則代表執行協程的棧幀資訊,libco為每一個協程在堆上分配了128k的空間作為該協程的棧幀。libco 把協程棧分為私有棧和共享棧:

在這裡插入圖片描述

協程在切換時會儲存棧內容,多個協程共用一片記憶體空間,即所謂共享棧,本質上是一個陣列,它裡面有count個元素,每一個元素都是指向一段記憶體的指標stStackMem_t,在新分配協程時(co_create_env),它會從剛剛分配的stShareStack_t 中,按一定的方式去一個stStackMem_t 出來,然後算作是協程自己的棧。因此稱為共享棧。兩種型別的棧各有優缺點:
對於共享棧來說,協程使用的棧空間可以開闢比較大,但每次拷貝需要額外消耗且棧地址不可跨協程使用;而對於私有棧來說,每個協程獨立執行與自己獨享的棧記憶體空間中,無需額外的拷貝棧記憶體且使用安全,但是可能會佔用較多的記憶體(作業系統對大的記憶體一般只分配地址空間,真實使用時才會觸發缺頁中斷,申請實體記憶體)。

4、libco的簡單應用

void* produce(void* arg);

void* consume(void* arg);

struct stParam_t
{
	//條件變數
	stCoCond_t* cond;
	//資料池
	std::vector<int> vec_data;
	//資料ID
	int data_id;
	//協程Id
	int coroutine_id;
};

int main()
{
	stParam_t p;
	p.cond=co_cond_alloc();
	p.coroutine_id=p.data_id=0;
	
	srand(time(NULL));
	//協程物件(CCB),一個生產者,多個消費者
	const int consumer_counts=2;
	stCoRoutine_t* producer_coroutine=NULL;
	
	stCoRoutine_t* consumers_coroutine[consumer_counts]={NULL};
	//建立並啟動生產者協程
	co_create(&producer_coroutine,NULL,produce,&p);
	co_resume(producer_coroutine);
	
	std::cout<<"start producer_coroutine success!"<<std::endl;
	
	//建立並啟動消費者協程
	for(int i=0;i<consumer_counts;i++)
	{
		co_create(&consumers_coroutine[i],NULL,consume,&p);
		co_resume(consumers_coroutine[i]);
	}	
	std::cout<<"start consumer_coroutine success!"<<std::endl;
	
	//啟動迴圈事件
	co_eventloop(co_get_epoll_ct(),NULL,NULL);
	return 0;
}

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

void* consume(void* arg)
{
	//啟動協程HOOK項
	co_enable_hook_sys();
	stParam_t* p=(stParam_t*)arg;
	int cid=++ p->coroutine_id;
	while(true)
	{
		//檢查資料池,無資料則等待通知
		if(p->vec_data.empty())
		{
			co_cond_timedwait(p->cond,-1);
			contine;
		}
		//消費資料
		std::cout<<"["<<cid<<"] - dele data:"<<p->vec_data.front()<<std::endl;
		p->vec_data.erase(p->vec_data.begin());
	}
	return NULL;
}

未完待續…

相關文章