tbox新增stackless协程支持
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);
// 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/
tbox新增stackless协程支持的更多相关文章
- Zend 官方框架增加 Swoole 协程支持 !
前言 Zend Framework 是 PHP 的官方框架,随着 Zend-Expressive-Swoole 0.2.2 的发布,率先支持了 Swoole 4 的协程功能,现在可以仅通过一个配置即可 ...
- tbox协程使用之切换与等待
tbox的协程实现,是stackfull模式的,需要指定独立堆栈和协程函数,目前暂时还不能像golang那样实现堆栈的动态增长,之后会对其进行支持. 目前提供下面一些功能特性: 1. 提供yield切 ...
- 协程分析之context上下文切换
协程现在已经不是个新东西了,很多语言都提供了原生支持,也有很多开源的库也提供了协程支持. 最近为了要给tbox增加协程,特地研究了下各大开源协程库的实现,例如:libtask, libmill, bo ...
- based on Greenlets (via Eventlet and Gevent) fork 孙子worker 比较 gevent不是异步 协程原理 占位符 placeholder (Future, Promise, Deferred) 循环引擎 greenlet 没有显式调度的微线程,换言之 协程
gevent GitHub - gevent/gevent: Coroutine-based concurrency library for Python https://github.com/gev ...
- spawn协程学习
对于IO密集型的程序,一般比较高效的做法是选择异步来实现,因为使用异步的方法更容易写出高效的程序.然而使用异步的话,经验较少的人往往会使自己的程序结构变得很混乱,进而导致程序的可读性变差.记得有人说过 ...
- 协程,greenlet原生协程库, gevent库
协程简介 协程(coroutine),又称为微线程,纤程,是一种用户级的轻量级线程.协程拥有自己的寄存器上下文和栈. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来时,恢复之前保存的上下文 ...
- python gevent 协程
简介 没有切换开销.因为子程序切换不是线程切换,而是由程序自身控制,没有线程切换的开销,因此执行效率高, 不需要锁机制.因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断 ...
- [转载]Python 3.5 协程究竟是个啥
http://blog.rainy.im/2016/03/10/how-the-heck-does-async-await-work-in-python-3-5/ [译] Python 3.5 协程究 ...
- [译] Python 3.5 协程究竟是个啥
转自:http://blog.rainy.im/2016/03/10/how-the-heck-does-async-await-work-in-python-3-5/ [译] Python 3.5 ...
随机推荐
- mongo配置
cmd下操作 cd /d E:\MongoDB\bin 创建数据库: mongod --dbpath E:\MongoDB\data\db 验证是否成功,登陆localhost:27017 进入E:\ ...
- web框架-(三)Django进阶
通过上节课的学习,我们已经对Django有了简单的了解,现在来深入了解下~ 1. 路由系统 1.1 单一路由对应 url(r'^index$', views.index), 1.2 基于正则的路由 u ...
- 1121. Damn Single (25)
"Damn Single (单身狗)" is the Chinese nickname for someone who is being single. You are suppo ...
- Netty学习第四章 spring boot整合netty的使用
现在大多数项目都是基于spring boot进行开发,所以我们以spring boot作为开发框架来使用netty.使用spring boot的一个好处就是能给将netty的业务拆分出来,并通过spr ...
- 「版本升级」界面控件Kendo UI正式发布R2 2019|附下载
通过70多个可自定义的UI组件,Kendo UI可以创建数据丰富的桌面.平板和移动Web应用程序.通过响应式的布局.强大的数据绑定.跨浏览器兼容性和即时使用的主题,Kendo UI将开发时间加快了50 ...
- frps启动
1.找到frps.ini文件 find / -name 'frps.ini' 2.窗口启动 ./frps -c ./frps.ini 3.关闭窗口,后台运行 setsid ./frps -c ./ ...
- git的初步研究1
工作中很多项目再往git上迁移,所以打算研究下git git是个版本控制系统 理解git工作区.暂存区.版本库的概念 工作区:在电脑中能看到的目录 暂存区:index即索引 即首先add加入暂存区 c ...
- IDEA提交代码到github
GIT客户端安装及idea配置github账号并提交代码到GIT参考资料:https://blog.csdn.net/qq_31405633/article/details/88193119 1. 选 ...
- java获取当前时间的年周月季度等的开始结束时间
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; /** * Created b ...
- Linux内核设计与实现 总结笔记(第十二章)内存管理
内核里的内存分配不像其他地方分配内存那么容易,内核的内存分配不能简单便捷的使用,分配机制也不能太复杂. 一.页 内核把页作为内存管理的基本单位,尽管处理器最小寻址坑是是字或者字节.但是内存管理单元MM ...