libco源碼解析(6) co_eventloop

libco源碼解析(1) 協程運行與基本結構
libco源碼解析(2) 創建協程,co_create
libco源碼解析(3) 協程執行,co_resume
libco源碼解析(4) 協程切換,coctx_make與coctx_swap
libco源碼解析(5) poll
libco源碼解析(6) co_eventloop
libco源碼解析(7) read,write與條件變量
libco源碼解析(8) hook機制探究
libco源碼解析(9) closure實現

引言

我們總能在運行libco協程代碼的最後看到對於函數co_eventloop的調用,它可以理解爲主協程執行的函數。我們舉一個簡單的例子來說明它的作用:


void* routinefun(void* args){
	co_enable_hook_sys();
	while(true){
		poll(NULL, 0, 1000);
	}
	return 0;
}

int main(int argc,char *argv[])
{
	vector<task_t> v;
	for(int i=1;i<argc;i+=2)
	{
		task_t task = { 0 };
		SetAddr( argv[i],atoi(argv[i+1]),task.addr );
		v.push_back( task );
	}

	for(int i=0;i<2;i++)
	{
		stCoRoutine_t *co = 0;
		co_create( &co,NULL,routinefun,v2 );
		printf("routine i %d\n",i);
		co_resume( co );
	}

	co_eventloop( co_get_epoll_ct(),0,0 );

	return 0;
}

這段代碼非常簡單,主協程運行兩個協程,協程函數所做的事情就是使用poll切換執行權,並在一秒後切換回來(超時)。這裏線程的執行過程是這樣的,我們把主協程看做A,其他兩個協程看做BC。執行過程爲:

  1. B協程執行,使用poll把一個stPoll_t結構插入時間輪,切換執行權,回到A協程。
  2. C協程執行,使用poll把一個stPoll_t結構插入時間輪,切換執行權,回到A協程。
  3. 此時A協程執行Eventloop中,不停的循環,直到B協程註冊的事件超時,調用回調回到B協程。
  4. B協程繼續執行,再次使用poll,重複第一步,回到A協程。
  5. A協程繼續執行Eventloop,不停的循環,直到C協程註冊的事件超時,調用回調回到C協程。
  6. C協程繼續執行,再次使用poll,重複第二步,回到A協程。

這樣我們就可以看清楚co_eventloop到底做了什麼,其實就是不停的輪詢等待其他協程註冊的事件成立,僅此而已。

co_eventloop

/*
* libco的核心調度
* 在此處調度三種事件:
* 1. 被hook的io事件,該io事件是通過co_poll_inner註冊進來的
* 2. 超時事件
* 3. 用戶主動使用poll的事件
* 所以,如果用戶用到了三種事件,必須得配合使用co_eventloop
*
* @param ctx epoll管理器
* @param pfn 每輪事件循環的最後會調用該函數
* @param arg pfn的參數
*/

void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{
	if( !ctx->result )	// 給結果集分配空間
	{													// epoll結果集大小
		ctx->result =  co_epoll_res_alloc( stCoEpoll_t::_EPOLL_SIZE );
	}
	co_epoll_res *result = ctx->result;


	for(;;)
	{
		// 最大超時時間設置爲 1 ms
		// 所以最長1ms,epoll_wait就會被喚醒
		int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );

		// 不使用局部變量的原因是epoll循環並不是元素的唯一來源.例如條件變量相關(co_routine.cpp stCoCondItem_t)
		stTimeoutItemLink_t *active = (ctx->pstActiveList);
		stTimeoutItemLink_t *timeout = (ctx->pstTimeoutList);

		memset( timeout,0,sizeof(stTimeoutItemLink_t) );

		// 獲取在co_poll_inner放入epoll_event中的stTimeoutItem_t結構體
		for(int i=0;i<ret;i++)
		{
			stTimeoutItem_t *item = (stTimeoutItem_t*)result->events[i].data.ptr;

			if( item->pfnPrepare ) // 如果用戶設置預處理回調的話就執行
			{
				// 若是hook後的poll的話,會把此事件加入到active隊列中,並更新一些狀態
				item->pfnPrepare( item,result->events[i],active );
			}
			else
			{
				AddTail( active,item );
			}
		}


		// 從時間輪上取出超時事件
		unsigned long long now = GetTickMS();

		// 以當前時間爲超時截止點
		// 從時間輪中取出超時的時間放入到timeout中
		TakeAllTimeout( ctx->pTimeout,now,timeout );

		stTimeoutItem_t *lp = timeout->head;
		while( lp ) // 遍歷超時鏈表,設置超時標誌,並加入active鏈表
		{
			//printf("raise timeout %p\n",lp);
			lp->bTimeout = true;
			lp = lp->pNext;
		}

		// 把timeout合併到active中
		Join<stTimeoutItem_t,stTimeoutItemLink_t>( active,timeout );

		lp = active->head;
		// 開始遍歷active鏈表
		while( lp )
		{
			// 在鏈表不爲空的時候刪除active的第一個元素 如果刪除成功,那個元素就是lp
			PopHead<stTimeoutItem_t,stTimeoutItemLink_t>( active );
            if (lp->bTimeout && now < lp->ullExpireTime) 
			{ // 一種排錯機制,在超時和所等待的時間內已經完成只有一個條件滿足纔是正確的
				int ret = AddTimeout(ctx->pTimeout, lp, now);
				if (!ret) //插入成功
				{
					lp->bTimeout = false;
					lp = active->head;
					continue;
				}
			}
			// TODO 有問題,如果同一個協程有兩個事件在一次epoll循環中觸發,
			// 那麼第一個事件切回去執行協程,第二個呢,已提交issue
			if( lp->pfnProcess )
			{	// 默認爲OnPollProcessEvent 會切換協程
				lp->pfnProcess( lp );
			}

			lp = active->head;
		}
		// 每次事件循環結束以後執行該函數, 用於終止協程
		if( pfn )
		{
			if( -1 == pfn( arg ) )
			{
				break;
			}
		}

	}
}

首先我們可以看到activetimeout鏈表都在stCoEpoll_t中存儲,而這個結構是線程私有的。那麼爲什麼不把這個值設置成局部變量呢?答案不在co_eventloop中,而藏在其他函數,比如libco實現的條件變量中,條件變量會在signal後把值放入到active鏈表或者timeout鏈表,而這些只能放在stCoEpoll_t中。

還有這裏的timeout鏈表其實最終會合併到active中,先分開純粹是爲了處理方便一點。

然後就是把事件從epoll結果集中拿出來,去執行預處理回調。我們來看看預處理回調,我們曾在poll中提到過:

void OnPollPreparePfn( stTimeoutItem_t * ap,struct epoll_event &e,stTimeoutItemLink_t *active )
{
	stPollItem_t *lp = (stPollItem_t *)ap;
	// 把epoll此次觸發的事件轉換成poll中的事件
	lp->pSelf->revents = EpollEvent2Poll( e.events );


	stPoll_t *pPoll = lp->pPoll;
	// 已經觸發的事件數加一
	pPoll->iRaiseCnt++;

	// 若此事件還未被觸發過
	if( !pPoll->iAllEventDetach )
	{
		// 設置已經被觸發的標誌
		pPoll->iAllEventDetach = 1;

		// 將該事件從時間輪中移除
		// 因爲事件已經觸發了,肯定不能再超時了
		RemoveFromLink<stTimeoutItem_t,stTimeoutItemLink_t>( pPoll );

		// 將該事件添加到active列表中
		AddTail( active,pPoll );

	}
}

我們可以看到其實所做的事情就是把epoll事件對應的stPoll_t結構中的值執行一些修改,並把此項插入到active鏈表中。

然後就是從時間輪中取出根據目前時間來說已經超時的事件,並插入到timeout鏈表中:

inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
{
	// 第一次調用是設置初始時間
	if( apTimeout->ullStart == 0 )
	{
		apTimeout->ullStart = allNow;
		apTimeout->llStartIdx = 0;
	}

	// 當前時間小於初始時間顯然是有問題的
	if( allNow < apTimeout->ullStart )
	{
		return ;
	}
	// 求一個取出事件的有效區間
	int cnt = allNow - apTimeout->ullStart + 1;
	if( cnt > apTimeout->iItemSize )
	{
		cnt = apTimeout->iItemSize;
	}
	if( cnt < 0 )
	{
		return;
	}
	for( int i = 0;i<cnt;i++)
	{	// 把上面求的有效區間過一遍,某一項存在數據的話插入到超時鏈表中
		int idx = ( apTimeout->llStartIdx + i) % apTimeout->iItemSize;
		// 鏈表操作,沒什麼可說的
		Join<stTimeoutItem_t,stTimeoutItemLink_t>( apResult,apTimeout->pItems + idx  );
	}
	// 更新時間輪屬性
	apTimeout->ullStart = allNow;
	apTimeout->llStartIdx += cnt - 1;
}

然後就是把超時鏈表處理以後加入到active鏈表啦,不得不說這些封裝的鏈表操作還是非常實用的。

然後就是遍歷active鏈表,一一執行每一個事件的回調啦,當然沒執行一次回調就意味着一次協程的切換,因爲我們在poll中註冊的回調執行co_resume。

這裏其實我個人認爲是有一些問題的。如果我們在poll中註冊了兩個fd的事件,這兩個時間在一次epoll_wait中被觸發,那麼第一個被執行了,第二個呢?如果再執行的話就會core dump,因爲這個上下文已經被用過了。這裏我們應該做一個哈希表,給每一個協程一個特定編號,在遍歷active時如果某一個協程已經被使用,我們在後面的遍歷過程不再調用回調,這樣就可以避免這個問題,已經提交issume。

我們注意到循環的最後調用了pfn,這是一個我們在調用co_eventloop時傳入的函數指針,它的作用是什麼呢?我的想法是跳出Eventloop循環,因爲不是所有的協程使用都想例子一樣把 co_eventloop放在函數最後,協程更多的是嵌到代碼中,我們需要在有些時候終止eventloop,傳入一個終止回調就是一個不錯的方法。

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