免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 9245 | 回复: 71

[C] tbox新增stackless协程支持 [复制链接]

论坛徽章:
1
程序设计版块每日发帖之星
日期:2015-11-08 06:20:00
发表于 2016-12-07 22:46 |显示全部楼层
tbox之前提供的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, tb_true);

        // 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);
这里只是举个例子,实际使用中尽量还是别这么直接用全局变量哦。。

论坛徽章:
15
射手座
日期:2014-11-29 19:22:4915-16赛季CBA联赛之青岛
日期:2017-11-17 13:20:09黑曼巴
日期:2017-07-13 19:13:4715-16赛季CBA联赛之四川
日期:2017-02-07 21:08:572015年亚冠纪念徽章
日期:2015-11-06 12:31:58每日论坛发贴之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-07-12 22:20:002015亚冠之浦和红钻
日期:2015-07-08 10:10:132015亚冠之大阪钢巴
日期:2015-06-29 11:21:122015亚冠之广州恒大
日期:2015-05-22 21:55:412015年亚洲杯之伊朗
日期:2015-04-10 16:28:25
发表于 2016-12-09 10:36 |显示全部楼层
本帖最后由 yulihua49 于 2016-12-09 10:57 编辑

以前跟猫讨论过这个问题。stackless对使用的限制不是一般的大,你的应用不可能不使用第三方软件。人家如何使用局部变量你根本无法控制。
我的解决办法依然是使用stackfull。但是使用栈池。
在服务器中,依然存在上万的协程(每个连接一个协程),但是不完整,在他们不活动时不配给栈。
只有这个连接收到请求时(epoll激活后),有一个线程接收了这个事件,才分配给他一个栈。然后进行NIO和业务处理。这个服务完成后线程收回栈,以便给下一个协程使用。
系统有N个工作线程,每个线程有两个栈,一个是线程自己的栈,一个是给应用协程的栈。这样,系统只需2N个栈。圆满解决问题。
已经在生产系统中使用这个模型。
工作非常可靠。转换速度没测,能够满足应用需求。使用makecontext,swapcontext实现的。
因为是多线程协程,使用方面还是有一些限制。因为在NIO之前和之后,可能不是一个线程,所以线程锁不可跨越。应使用基于协程的乐观锁。


论坛徽章:
1
程序设计版块每日发帖之星
日期:2015-11-08 06:20:00
发表于 2016-12-15 22:53 |显示全部楼层
回复 2# yulihua49

tbox里面也提供有stackfull协程的实现,参考了boost.context的切换内核实现,并且在其基础上进行了优化,比swapcontext效率高很多,至少快了10倍。
用户是否使用stackless还是stackfull可根据实际情况而定,一般应用场景确实还是stackfull比较适合。。
但是如果用户能够很好地驾驭stackless模式,并且应用场景也不是很复杂的需求,那么使用stackless还是不错的选择,尤其是嵌入式环境。。例如:tbox里面内置的轻量stackless版本http server实现,基本上就可以完全满足部分应用需求。

另外tbox里面的stackfull模式,对栈利用也是进行过复用的,一个协程结束后,其他协程会复用栈池里面的空闲栈,而且默认模式下,如果没有io操作,则不会开启epoll的io调度,知道实际使用io进行wait的时候,才会启用,并且对io的频繁wait,协程内部对io事件进行了缓存,减少了大量频繁的冗余epoll_ctl,效率提升非常明显。。


并且通过环切的方式,避免了像libtask这种需要中心调度,导致的冗余切换。。

论坛徽章:
15
射手座
日期:2014-11-29 19:22:4915-16赛季CBA联赛之青岛
日期:2017-11-17 13:20:09黑曼巴
日期:2017-07-13 19:13:4715-16赛季CBA联赛之四川
日期:2017-02-07 21:08:572015年亚冠纪念徽章
日期:2015-11-06 12:31:58每日论坛发贴之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-07-12 22:20:002015亚冠之浦和红钻
日期:2015-07-08 10:10:132015亚冠之大阪钢巴
日期:2015-06-29 11:21:122015亚冠之广州恒大
日期:2015-05-22 21:55:412015年亚洲杯之伊朗
日期:2015-04-10 16:28:25
发表于 2016-12-18 15:01 |显示全部楼层
本帖最后由 yulihua49 于 2016-12-18 15:10 编辑
waruqi 发表于 2016-12-15 22:53
回复 2# yulihua49

tbox里面也提供有stackfull协程的实现,参考了boost.context的切换内核实现,并且在 ...

我的观点,如无IO则无需协程。它一定是在IO环境。
因此,epoll是肯定启用的,而且是整个作业核心。
协程,只是线程池服务器框架的补充。

在各线程等待epoll期间,因为没有任务,所以使用线程栈。只有激活后,接受了任务,才需要为该任务分配协程栈。此后将栈交换到这个协程栈。当这个协程需要等待IO时,挂起这个协程栈,恢复线程栈,线程转移为别的任务服务。这个协程IO达成后,有新的线程接手,交换进这个任务的协程栈。直至这个任务彻底完成,回收他的栈,把他的fd丢给epoll,等待下一次通讯。

论坛徽章:
1
程序设计版块每日发帖之星
日期:2015-11-08 06:20:00
发表于 2016-12-19 21:42 |显示全部楼层
回复 4# yulihua49

协程只是一种实现轻量级任务切换调度的机制,并不限制在io调度上,也不限制在服务端框架上,虽然大部分情况下都是用于io处理。
客户端的并发io请求也是可以使用协程,多进程的等待调度控制也是可以使用协程,嵌入式上的一些信号控制和调度也可以使用轻量的协程,而不是线程去操作,这些场景,并不需要epoll/poll来调度的。。

论坛徽章:
15
射手座
日期:2014-11-29 19:22:4915-16赛季CBA联赛之青岛
日期:2017-11-17 13:20:09黑曼巴
日期:2017-07-13 19:13:4715-16赛季CBA联赛之四川
日期:2017-02-07 21:08:572015年亚冠纪念徽章
日期:2015-11-06 12:31:58每日论坛发贴之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-07-12 22:20:002015亚冠之浦和红钻
日期:2015-07-08 10:10:132015亚冠之大阪钢巴
日期:2015-06-29 11:21:122015亚冠之广州恒大
日期:2015-05-22 21:55:412015年亚洲杯之伊朗
日期:2015-04-10 16:28:25
发表于 2016-12-20 11:04 |显示全部楼层
本帖最后由 yulihua49 于 2016-12-20 11:09 编辑
waruqi 发表于 2016-12-19 21:42
回复 4# yulihua49

协程只是一种实现轻量级任务切换调度的机制,并不限制在io调度上,也不限制在服务端 ...

协程本身与IO无关。可是作为用户,在非IO场合,有何必要使用协程?还要使用stackless协程,受多大限制啊。我没有发现这个必要性。
你能跟我说说非IO场合使用协程的必要性吗?

论坛徽章:
15
射手座
日期:2014-11-29 19:22:4915-16赛季CBA联赛之青岛
日期:2017-11-17 13:20:09黑曼巴
日期:2017-07-13 19:13:4715-16赛季CBA联赛之四川
日期:2017-02-07 21:08:572015年亚冠纪念徽章
日期:2015-11-06 12:31:58每日论坛发贴之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-07-12 22:20:002015亚冠之浦和红钻
日期:2015-07-08 10:10:132015亚冠之大阪钢巴
日期:2015-06-29 11:21:122015亚冠之广州恒大
日期:2015-05-22 21:55:412015年亚洲杯之伊朗
日期:2015-04-10 16:28:25
发表于 2016-12-20 11:13 |显示全部楼层
waruqi 发表于 2016-12-19 21:42
回复 4# yulihua49

协程只是一种实现轻量级任务切换调度的机制,并不限制在io调度上,也不限制在服务端 ...

我那个方案已经运用在服务器并发IO和客户端并发IO环节。而且支持多个线程池和多种协程切换机制,在同一个进程里。

论坛徽章:
15
射手座
日期:2014-11-29 19:22:4915-16赛季CBA联赛之青岛
日期:2017-11-17 13:20:09黑曼巴
日期:2017-07-13 19:13:4715-16赛季CBA联赛之四川
日期:2017-02-07 21:08:572015年亚冠纪念徽章
日期:2015-11-06 12:31:58每日论坛发贴之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-07-12 22:20:002015亚冠之浦和红钻
日期:2015-07-08 10:10:132015亚冠之大阪钢巴
日期:2015-06-29 11:21:122015亚冠之广州恒大
日期:2015-05-22 21:55:412015年亚洲杯之伊朗
日期:2015-04-10 16:28:25
发表于 2016-12-20 11:17 |显示全部楼层
本帖最后由 yulihua49 于 2016-12-20 11:18 编辑
waruqi 发表于 2016-12-19 21:42
回复 4# yulihua49

协程只是一种实现轻量级任务切换调度的机制,并不限制在io调度上,也不限制在服务端 ...

还一个问题请教下,lockless机制构建生产者/消费者模型时,如果消费者得不到资源,怎么等待呢?不会总是消耗cpu吧?
常规的方法是等锁。

论坛徽章:
1
程序设计版块每日发帖之星
日期:2015-11-08 06:20:00
发表于 2016-12-20 11:22 |显示全部楼层
回复 6# yulihua49

可以看下我的另外一个项目:xmake,里面就是用协程实现多任务编译,通过协程调度等待多个编译子进程,使其保证在同一时刻存在固定数量的后台编译进程
用协程的话,控制逻辑上非常方便,而且所有进程都在运行时,会去使用类似epoll的接口多进程wait去等待多个进程对象,也不需要额外起多个线程进行维护和同步。

效率和资源使用都能做到很好的权衡,而且可读性也非常好。

论坛徽章:
1
程序设计版块每日发帖之星
日期:2015-11-08 06:20:00
发表于 2016-12-20 11:25 |显示全部楼层
而且我并没说必须要使用stackless协程,我的库里面stackfull和stackless都实现了,根据自己的实际需求,可针对性使用不同的协程模式。。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP