tbox新增stackless協程支持

tbox以前提供的stackfull協程庫,雖然切換效率已經很是高了,可是因爲每一個協程都須要維護一個獨立的堆棧, 內存空間利用率不是很高,在併發量很是大的時候,內存使用量會至關大。git

以前考慮過採用stacksegment方式進行內存優化,實現動態增漲,可是這樣對性能仍是有必定的影響,暫時不去考慮了。github

最近參考了下boost和protothreads的stackless協程實現,這種方式雖然易用性和靈活性上受到了不少限制,可是對切換效率和內存利用率的提高效果仍是很是明顯的。。macos

所以,我在tbox裏面也加上了對stackless協程的支持,在切換原語上參考了protothreads的實現,接口封裝上參考了boost的設計,使得更加可讀易用數據結構

先曬段實際的接口使用代碼:併發

tb_lo_coroutine_enter(coroutine)
{
    while (1)
    {
        tb_lo_coroutine_yield();
    }
}

而後實測對比了下:less

* 切換性能在macosx上比tbox的stackfull版本提高了5-6倍,1000w次切換隻須要40ms
* 每一個協程的內存佔用也減小到了只有固定幾十個bytes

那麼既然stackless的效率提高這麼明顯,stackfull模式還須要嗎?能夠比較下二者的優劣:socket

  • stackfull協程:易用性和靈活性很是高,可是內存使用過大
  • stackless協程:切換效率和內存利用率很高,更加輕量,可是使用上限制較多

因爲stackless的實現比較輕量,佔用資源也不是不少,所以tbox默認放置到了micro微內核模式下,做爲基礎模塊,提供股嵌入式平臺使用函數

而通常狀況下,若是對資源使用和切換性能要求不是很是苛刻的話,使用stackfull的方式會更加方便,代碼也更易於維護oop

具體如何選擇,可根據實際使用場景,本身選擇哦。。性能

切換

下面給的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開源工程

相關文章
相關標籤/搜索