之前提供的stackfull協程庫,雖然切換效率已經非常高了,但是由于每個協程都需要維護一個獨立的堆棧,
記憶體空間使用率不是很高,在并發量非常大的時候,記憶體使用量會相當大。
之前考慮過采用stacksegment方式進行記憶體優化,實作動态增漲,但是這樣對性能還是有一定的影響,暫時不去考慮了。
最近參考了下boost和protothreads的stackless協程實作,這種方式雖然易用性和靈活性上受到了很多限制,但是對切換效率和記憶體使用率的提升效果還是非常明顯的。。
是以,我在tbox裡面也加上了對stackless協程的支援,在切換原語上參考了protothreads的實作,接口封裝上參考了boost的設計,使得更加可讀易用
先曬段實際的接口使用代碼:
tb_lo_coroutine_enter(coroutine)
{
while (1)
{
tb_lo_coroutine_yield();
}
}
然後實測對比了下:
* 切換性能在macosx上比tbox的stackfull版本提升了5-6倍,1000w次切換隻需要40ms
* 每個協程的記憶體占用也減少到了隻有固定幾十個bytes
那麼既然stackless的效率提升這麼明顯,stackfull模式還需要嗎?可以比較下兩者的優劣:
- stackfull協程:易用性和靈活性非常高,但是記憶體使用過大
- stackless協程:切換效率和記憶體使用率很高,更加輕量,但是使用上限制較多
由于stackless的實作比較輕量,占用資源也不是很多,是以tbox預設放置到了micro微核心模式下,作為基礎子產品,提供股嵌入式平台使用
而一般情況下,如果對資源使用和切換性能要求不是非常苛刻的話,使用stackfull的方式會更加友善,代碼也更易于維護
具體如何選擇,可根據實際使用場景,自己選擇哦。。
切換
下面給的tbox的stackless協程切換執行個體,直覺感受下:
static tb_void_t switchtask(tb_lo_coroutine_ref_t coroutine, tb_cpointer_t priv)
{
// check
tb_size_t* count = (tb_size_t*)priv;
// enter coroutine
tb_lo_coroutine_enter(coroutine)
{
// loop
while ((*count)--)
{
// trace
tb_trace_i("[coroutine: %p]: %lu", tb_lo_coroutine_self(), *count);
// yield
tb_lo_coroutine_yield();
}
}
}
static tb_void_t test()
{
// init scheduler
tb_lo_scheduler_ref_t scheduler = tb_lo_scheduler_init();
if (scheduler)
{
// start coroutines
tb_size_t counts[] = {10, 10};
tb_lo_coroutine_start(scheduler, switchtask, &counts[0], tb_null);
tb_lo_coroutine_start(scheduler, switchtask, &counts[1], tb_null);
// run scheduler
tb_lo_scheduler_loop(scheduler);
// exit scheduler
tb_lo_scheduler_exit(scheduler);
}
}
其實整體接口使用跟tbox的那套stackfull接口類似,并沒有多少差別,但是相比stackfull還是有些限制的:
1. 目前隻能支援在根函數進行協程切換和等待,嵌套協程不支援
2. 協程内部局部變量使用受限
對于限制1,我正在研究中,看看有沒有好的實作方案,之前嘗試過支援下,後來發現需要按棧結構分級儲存每個入口的label位址,這樣會占用更多記憶體,就放棄了。
對于限制2,由于stackless協程函數是需要重入的,是以目前隻能在enter()塊外部定以一些狀态不變的變量,enter()塊内部不要使用局部變量
接口設計上,這邊采用boost的模式:
// enter coroutine
tb_lo_coroutine_enter(coroutine)
{
// yield
tb_lo_coroutine_yield();
}
這樣比起protothreads的那種begin()和end(),更加可讀和精簡,接口也少了一個。。
參數傳遞
tb_lo_coroutine_start
的最後兩個參數,專門用來傳遞關聯每個協程的私有資料priv和釋放接口free,例如:
typedef struct __tb_xxxx_priv_t
{
tb_size_t member;
tb_size_t others;
}tb_xxxx_priv_t;
static tb_void_t tb_xxx_free(tb_cpointer_t priv)
{
if (priv) tb_free(priv);
}
static tb_void_t test()
{
tb_xxxx_priv_t* priv = tb_malloc0_type(tb_xxxx_priv_t);
if (priv)
{
priv->member = value;
}
tb_lo_coroutine_start(scheduler, switchtask, priv, tb_xxx_free);
}
上述例子,為協程配置設定一個私有的資料結構,用于資料狀态的維護,解決不能操作局部變量的問題,但是這樣寫非常繁瑣
tbox裡面提供了一些輔助接口,用來簡化這些流程:
typedef struct __tb_xxxx_priv_t
{
tb_size_t member;
tb_size_t others;
}tb_xxxx_priv_t;
static tb_void_t test()
{
// start coroutine
tb_lo_coroutine_start(scheduler, switchtask, tb_lo_coroutine_pass1(tb_xxxx_priv_t, member, value));
}
這個跟之前的代碼功能上是等價的,這裡利用
tb_lo_coroutine_pass1
宏接口,自動處理了之前的那些設定流程,
用來快速關聯一個私有資料塊給新協程。
挂起和恢複
這個跟stackfull的接口用法上也是一樣的:
tb_lo_coroutine_enter(coroutine)
{
// 挂起目前協程
tb_lo_coroutine_suspend();
}
// 恢複指定協程(這個可以不在協程函數内部使用,其他地方也可以調用)
tb_lo_coroutine_resume(coroutine);
挂起和恢複跟yield的差別就是,yield後的協程,之後還會被切換回來,但是被挂起的協程,除非調用resume()恢複它,否則永遠不會再被執行到。
等待
當然一般,我們不會直接使用suspend()和resume()接口,這兩個比較原始,如果需要定時等待,可以使用:
tb_lo_coroutine_enter(coroutine)
{
// 等待1s
tb_lo_coroutine_sleep(1000);
}
來挂起目前協程1s,之後會自動恢複執行,如果要進行io等待,可以使用:
static tb_void_t tb_demo_lo_coroutine_client(tb_lo_coroutine_ref_t coroutine, tb_cpointer_t priv)
{
// check
tb_demo_lo_client_ref_t client = (tb_demo_lo_client_ref_t)priv;
tb_assert(client);
// enter coroutine
tb_lo_coroutine_enter(coroutine)
{
// read data
client->size = sizeof(client->data) - 1;
while (client->read < client->size)
{
// read it
client->real = tb_socket_recv(client->sock, (tb_byte_t*)client->data + client->read, client->size - client->read);
// has data?
if (client->real > 0)
{
client->read += client->real;
client->wait = 0;
}
// no data? wait it
else if (!client->real && !client->wait)
{
// 等待socket資料
tb_lo_coroutine_waitio(client->sock, TB_SOCKET_EVENT_RECV, TB_DEMO_TIMEOUT);
// 擷取等到的io事件
client->wait = tb_lo_coroutine_events();
tb_assert_and_check_break(client->wait >= 0);
}
// failed or end?
else break;
}
// trace
tb_trace_i("echo: %s", client->data);
// exit socket
tb_socket_exit(client->sock);
}
}
這個跟stackfull模式除了局部變量的差別,其他使用上幾乎一樣,也是同步模式,但是實際上tbox已經在底層把它放入了poller輪詢器中進行等待
在沒有資料,調用
tb_lo_coroutine_waitio
進行socket等待事件後,tbox會自動啟用stackless排程器内部的io排程器(預設是不啟用的,延遲加載,減少無畏的資源浪費)
然後進行poll切換排程(内部根據不同平台使用epoll, kqueue, poll, 後續還會支援iocp)。
如果有事件到來,會将收到事件的所有協程恢複執行,當然也可以指定等待逾時,逾時傳回或者強行kill中斷掉。
tbox中内置了一個stackless版本的
http_server,實作也是非常輕量,經測試效率還是非常高的,
整體表現比stackfull的實作更好。
更多stackless接口使用demo,可以參考tbox的
源碼信号量和鎖
這個就簡單講講了,使用跟stackfull的類似,例如:
// the lock
static tb_lo_lock_t g_lock;
// enter coroutine
tb_lo_coroutine_enter(coroutine)
{
// loop
while (lock->count--)
{
// enter lock
tb_lo_lock_enter(&g_lock);
// trace
tb_trace_i("[coroutine: %p]: enter", tb_lo_coroutine_self());
// wait some time
tb_lo_coroutine_sleep(1000);
// trace
tb_trace_i("[coroutine: %p]: leave", tb_lo_coroutine_self());
// leave lock
tb_lo_lock_leave(&g_lock);
}
}
// init lock
tb_lo_lock_init(&g_lock);
// start coroutine
// ..
// exit lock
tb_lo_lock_exit(&g_lock);
這裡隻是舉個例子,實際使用中盡量還是别這麼直接用全局變量哦。。
個人首頁:
TBOOX開源工程原文出處:
http://tboox.org/cn/2016/12/03/stackless-coroutine/