项目链接:https://github.com/hnes/libaco
大家好,下面是这个项目的简要介绍:
上文中的"最快"指的是在满足 Sys V ABI Intel386 或者 AMD64 约束下最快的上下文切换实现。
中文文档正在做最后的 review,今晚或者明天发布,敬请期待 :D
下面是前几天在 Reddit 上讨论的帖子列表:
热烈欢迎大家的问题和建议 ;-)
Edit: 添加项目链接。
1
lekai63 2018-07-17 20:28:34 +08:00 via iPhone 1
沙发。看起来很吊的样子
|
3
byteli 2018-07-17 20:39:43 +08:00 via Android 1
板凳,支持一下,收藏了有空看看。文档看起来非常优秀
|
5
Kilerd 2018-07-17 21:43:53 +08:00 1
6666. mark 最近刚好想研究一下协程在底层的具体实现。有空读下代码。
|
6
hnes OP @Kilerd 十分感谢哈 ;-)
协程的实现以及应用确实非常有趣,希望这个项目的代码以及文档能够对你有用处,如果遇到任何问题可以发 Issue 大家一起讨论 😁 |
7
icylord 2018-07-17 22:47:15 +08:00 1
厉害,协程一直都处于看不懂的状态
|
8
hnes OP @icylord 感谢 :D
协程的根本思想应该是比较清晰的。 但如果要具体地去实现一个协程库,需要对系统的 ABI 规范很熟悉(重点是二进制函数调用中寄存器与栈的一些约定细节),也需要些汇编(重点是函数调用约定部分)的基础知识(这部分其实比较简单,如果对计算机体系结构比较熟悉的话初学它可能一两个小时就学会了)。 比如腾讯的 libco 中就有一个[bug]( https://github.com/Tencent/libco/issues/90),在微信的生产系统中隐藏了五年多都没有被发现(从它的 Github 代码仓库中看是如此),就是因为在实现的时候没有严格遵守 Sys V ABI 的规范导致的。 libaco 项目中的文档还是比较齐全的,而且还有严格的数学证明,希望对你有所助益哈 ;-) 当然,如果有问题的话,可以发 Issue 大家一起探讨,这样会更加有乐趣 :D |
9
hnes OP 实在抱歉,纠正:
链接: https://github.com/Tencent/libco/issues/90 > 比如腾讯的 libco 中就有一个 bug,在微信的生产系统中隐藏了五年多都没有被发现(从它的 Github 代码仓库中看是如此),就是因为在实现的时候没有严格遵守 Sys V ABI 的规范导致的。 |
10
lsmgeb89 2018-07-17 23:29:51 +08:00 1
有证明厉害,看看
|
11
tommydong 2018-07-17 23:39:16 +08:00 via iPhone 1
好奇的问句 1000 万的协程并发的应用场景?
|
12
tommydong 2018-07-17 23:39:54 +08:00 via iPhone 1
还是很厉害👍 要赞一个
|
13
jedihy 2018-07-18 01:20:18 +08:00 1
厉害
|
14
est 2018-07-18 01:45:51 +08:00 via Android 1
厉害
|
15
hnes OP @tommydong 早上好,实在不好意思哈,昨天回复完上一位朋友的留言后就去睡觉了 ;-)
> 1000 万的协程并发的应用场景? 一个简短的总结就是,高性能、高并发和对内存使用效率要求很高的场景(当然,对这三点要求低些的应用肯定都是能用的,哈哈)。 一个最典型的案例就是网络服务程序,比如大型集群的前端调度器(单机并发数百万数量级,比如像以用户空间 dpdk 实现的 Director ),以及像 Nginx、Redis 这样的对以上三点要求很高的网络服务基础设施。 穿插一些相关的: Golang 也很棒,但是却不太适合上面的场景,与 C/C++相比,Golang 的编译器的优化有较大的提升空间(且 GCC 已经有 30 多年的演进历史),语言的抽象层过于重量级( Golang 的核心开发小组为了用户的易于使用已经牺牲了太多的性能,在多线程高并发的场景下,调度器损耗太大,当然还有 GC 问题...)。 我其实也是 Golang 的长期用户和爱好者,如果碰到对这些指标要求不是很高的服务程序我还是会很乐意选择 Golang 的 ;-) |
18
pymumu 2018-07-18 08:50:35 +08:00 via Android 1
这个轮子对比 libc 的 ucontext 协程有什么优势?
|
19
missdeer 2018-07-18 09:03:45 +08:00 1
支持什么系统?什么编译器?
|
20
hnes OP @pymumu
libaco 的上下文切换`acosw`耗时为 10ns (RHEL-7.5 & c5d.large on AWS),而 ucontext 中有关于 sigprocmask 的系统调用(而且对于应用来说,在绝大多数情况下这个 Syscall 是没有存在必要的),时间数量级在 200-300ns 之间,而且系统调用会给当前运行线程带来严重的 cache-miss,会进一步影响线程的运行,所以,真正注重性能的网络应用是不会选择使用 ucontext (还有一点,ucontext 中的 setcontext 等等函数已经从 POSIX 标准中移除)。 PS: $ man 3 setcontext ... CONFORMING TO SUSv2, POSIX.1-2001. POSIX.1-2008 removes the specification of getcontext(), citing portability issues, and recommending that applications be rewritten to use POSIX threads instead. ... 另外,OpenBSD 中并没有实现 ucontext。 |
21
hnes OP @pymumu 另外,10ns 在 3.5GHz 的 CPU 上大概只有 35 个时钟周期,这仅仅约为一个 32 位整型除法指令的耗时。
Intel Haswell: instruction latency (core clock cycles) add 1 mul 3 div r32 22-29 div r64 32-96 |
22
hnes OP @missdeer 目前支持所有 ABI 规范为 Sys V ABI Intel386 以及 AMD64 的操作系统,包括所有 Unix 系的操作系统,比如 Linux, BSD, MacOS, Minix, SunOS...等等。
具体只要查询一下目标系统的 ABI 规范是否为 Sys V ABI Intel386 或 AMD64 即可 ;-) |
23
hnes OP @missdeer 编译器的话目前已知可以用的有 Gcc 和 Clang,正常情况下编译工具链只要包含 C 编译器和汇编器(还有链接器)应该都没有问题,当然具体的可以尝试一下哈。
|
24
hnes OP @missdeer 后面也会加入对 ARM 的支持,以及对 Windows 操作系统的支持( Microsoft ABI ),敬请关注 :D
|
25
pymumu 2018-07-18 09:33:37 +08:00 1
@hnes 你总结的没错,ucontext 上下文切换是相对比较耗时的。
posix 移除此接口原因是 makecontext 函数与 ISO C 不兼容,这个两个有差别吗? c 的协程对编程的要求比较高,如果切换不合理,一不小心性能反而会下降,并且协程如果调用系统调用阻塞了,那整个任务也就挂住了。并且现在多核的情况下,协程并不能有效利用 CPU。 协程的本意是用户态调度,目的是让写代码的人,感知不到切换,也就是一个上下文做一件事情,没有状态,这是对比异步 IO+多路复用来说的。 但我觉得对于高并发程序,还是应该使用异步 IO+多路复用的模式来写,因为底层 API 对异步 IO 已经支持很好了。支持几十万,上百万处理不是问题。 go routine 在这方面其实优化的很好,不仅保证了写代码的便捷,同时保证了性能。 如果这个库能让 C 有类似的能力的话,就很好了。否则也只是特定的业务使用,并且还要小心使用。 |
26
tt67wq 2018-07-18 09:34:27 +08:00 1
碉堡了!
|
27
feverzsj 2018-07-18 09:34:51 +08:00 1
可以和 vc 现在支持的 c++ stackless coroutine 做下性能比较
|
28
hnes OP @pymumu 感谢你的建议 :D
> c 的协程对编程的要求比较高,如果切换不合理,一不小心性能反而会下降,并且协程如果调用系统调用阻塞了,那整个任务也就挂住了。 是这样的,但是协程的使用目标一般都是借此实现类似 ngx lua cosocket 的这种编程模型的(用同步的语义书写异步程序,彻底摆脱所谓的 callback hell ),后面我还会继续放出来一个满足这个要求的库,可以认为是一个 C 语言实现的“ Golang ”定制版(砍掉 golang 中不适合高性能高并发低消耗场景的一些东西,尤其是复杂重量级的调度器和 GC 逻辑) :D ngx lua cosocket: https://moonbingbing.gitbooks.io/openresty-best-practices/ngx_lua/whats_cosocket.html > 并且现在多核的情况下,协程并不能有效利用 CPU 但是,为什么不能是多线程+协程(协程数 : 线程数 == C*T : T )呢? > 如果这个库能让 C 有类似的能力的话,就很好了。否则也只是特定的业务使用,并且还要小心使用。 是的,后面将会放出一个这样的库,完全满足你的设想 :D (之所以打算两个分开放出来,是因为我认为协程库是很多场景中都有很大用处的一个组件,分开放出来应该对使用者们更有利,可以自由的定制选择) |
30
tommydong 2018-07-18 09:56:43 +08:00 via iPhone 1
@hnes 这种情况在 iass 的 LB 服务里面的确需要 ,我们以前就是基于 dpdk 做的。dpdk 是绕过了 linux 的内核协议栈,这个库不知道有没有做这方面的工作
|
32
hnes OP @tommydong 之前研究过集群前端调度器很长时间,不过现在我没有在从事这方面的工作。
但是 libaco 是通用的,自然可以自由的与 dpdk 结合在一起,后续我还会放出来一个类 Golang 的 C 多线程网络库(前面给 pymumu 的回复中有具体的描述),只要修改一下底层 socket 的 API,应该就能直接的用在 dpdk 上面了。 |
34
deadEgg 2018-07-18 19:02:57 +08:00 1
支持 ,以前被 GIL 搞的要死才用协程。
请教楼主现在做这个库是为了解决什么场景的问题呢。还是说去解决一些性能上的问题呢? |
35
hnes OP @deadEgg 非常感谢你的支持 ;-)
> 现在做这个库是为了解决什么场景的问题呢。还是说去解决一些性能上的问题呢? 是的,主要是性能上的问题,但是,也可以认为是某种场景的解决方案。 场景: 高性能(应该接近极致)、高并发(百万数量级)和对内存使用效率(应该接近极致)要求很高的应用。 我们要实现一个核心的网络服务,最初考察使用 go 来完成,后来经过了一段时间的考察实验,甚至已经写了很多的 go 代码,发现完全满足不了我们需求( GC、多线程切换损耗、内存消耗、调度器相关的等等相关的问题),最后还是决定选择使用 C 来实现。 而且在网络编程时又不想过度的使用 callback 去手写状态机,所以最好的方法就是实现一个“用 C 定制的 golang ”,去掉 golang 对高性能高并发应用场景不利的的东西,在 C 的极速与 golang 的易用之间找到一个最适合我们应用场景的平衡点,而 libaco 就是这个项目中的核心组件--协程库的实现,后续将会把全部的实现代码都放出来,敬请期待 :D PS: 我也是 go 的爱好者,如果遇到要求不是这么高的场景,还是非常乐意选择 go 的。 |
36
wtcoder 2018-07-19 02:44:07 +08:00 1
楼主牛人!小公司表示看了您作品的介绍,才发现~ 我们最大的性能瓶颈是带(Que)宽(Qian)...
|
37
hnes OP |
38
yulon 2018-07-19 16:01:38 +08:00 1
libco 里面居然还有这种操作,我被秀到了_🤣」∠)_
|
40
pkv 2018-07-19 17:45:52 +08:00 1
有两个小问题:
1. 有没有在生产环境使用? 2. 有没有和过其它 C/C++ 协程库的对比结果,不只是自己跑的 benchmark,如对比微信使用的 https://github.com/Tencent/libco 谢谢~ |
41
hnes OP @pkv 十分感谢你的支持 :D
实在很抱歉这么晚才回复你 :-) 两个问题我分开回复了哈。 > 有没有和过其它 C/C++ 协程库的对比结果,不只是自己跑的 benchmark,如对比微信使用的 libco 第一点,腾讯的 libco 的实现中有严重的 bug (堆上的内存会被随机性的破坏),对于对正确性要求很高的应用不推荐使用,否则程序可能在运行过程中做很多令人意想不到的任何事情。 第二点,即使是和实现错误的腾讯 libco 相比,libaco 的协程上下文切换汇编实现 acosw 的速度是腾讯 libco 的 coctx_swap 的 1.7 倍。libaco 的分支中有相关的性能对比报告和代码。 (实在抱歉,回复中不能加链接了,相关的链接可以在帖子最上方的附言中找到) |
42
hnes OP @pkv
> 有没有在生产环境使用? 有,但是用户量目前没有微信那么大。 尽管还没有像微信那样大量的用户来做生产中的 Beat 测试,但下面有一些你可能会觉得很有用的统计数据: 在 libco 的核心中只有不超过 700 行的代码,但是花了我一个多月的时间才编写完成它的第一个版本,还有完整的一个月时间来进行非常严格的代码审查(几乎每一行代码都被严格地证明和推理过,并且被审查证明过很多次),当然,还有全面的测试。libaco 是我们正在开发的产品中最重要的核心组件之一,因此我们对它投入了很大的精力和注意力。 还有一件事,有时来自巨量用户的 Beat 测试并不总是足够的,例如刚才我们提到的微信 libco 的案例,对于这种重要的短小精悍的基础设施软件,只有通过严格的代码审查、对系统的深刻理解和正确的设计才能做到 bug free,测试(包括生产中的应用测试)一般只能检测出很低级的错误。 我相信 libco 的生产使用会随着时间的推移而逐渐增加,而且,在 libaco 的刚刚发布的 20 天不到,已经有国外的公司开始准备在生产中使用它了。 |
43
hnes OP @pkv 另外,关于 libaco 的速度问题,libaco 的 acosw 协程上下文切换汇编是在 Sys V ABI intel386 或 AMD64 的约束之下的最快且正确的实现,这是经过数学证明了的,具体可以阅读 libaco 文档中的数学证明部分。
感谢你的关注 ;-) |
44
hnes OP @hnes
勘误: > 在 **libco** 的核心中只有不超过 700 行的代码,但是花了我一个多月的时间才编写完成它的第一个版本,还有完整的一个月时间来进行非常严格的代码审查(几乎每一行代码都被严格地证明和推理过,并且被审查证明过很多次),当然,还有全面的测试。libaco 是我们正在开发的产品中最重要的核心组件之一,因此我们对它投入了很大的精力和注意力。 **libco** -> libaco 十分抱歉... |
45
pkv 2018-07-20 10:20:54 +08:00 1
@hnes 很棒的工作,赞!!!牛逼!!!
这么短时间内,有公司敢用,也是勇气可嘉。 数学证明很牛逼,在这里没多少人能看懂吧,即使是写论文,最终也得拿个数据说话。 Q:问个小白问题,我理解底层是基于 epoll 类似的事件机制,libaco 中实现的类似 Go 语言中 goroutine 调度器的角色,libaco 其实是降低用户实现高并发的难度,以类同步编程实现异步,用户友好,如果有能力做到类似 Nginx 这种高效的使用,应该没必要使用协程的对吗? |
46
hnes OP > 很棒的工作,赞!!!牛逼!!!
十分感谢你的鼓励 :D > 这么短时间内,有公司敢用,也是勇气可嘉。 > 数学证明很牛逼,在这里没多少人能看懂吧 这个我有些不赞同哈,libaco 核心只有 700 行代码,如果对 ABI 规范和汇编有一定了解的程序员,第一次几乎半个小时不到就能够读完了,就算是对 ABI 与汇编有些生疏,只要温习一下这方面的知识一样可以轻松读懂。另外,数学证明其实是非常简单的,有不少朋友向我反馈他们只是看了一下文档中的几张示意图就完全明白了 libaco 的设计思想和原理。 程序某种程度上说就是数学问题,证明当然永远是第一位的。 > 即使是写论文,最终也得拿个数据说话 数据,指的是?文档中有详细的 benchmark 方法和结果报告。如果是指用户量的话,任何软件产品都是先发布,然后才能有用户量,不是么? > Q:问个小白问题,我理解底层是基于 epoll 类似的事件机制,libaco 中实现的类似 Go 语言中 goroutine 调度器的角色,libaco 其实是降低用户实现高并发的难度,以类同步编程实现异步,用户友好 赞,正是如此 ;-) 下一个将要发布的开源项目正是完成了你所陈述的所有事情,热烈欢迎继续关注哈 :D > 如果有能力做到类似 Nginx 这种高效的使用,应该没必要使用协程的对吗? 是这样的,如果喜欢使用 callback 方式写网络应用并且愿意付出相应的开发代价,当然是没有任何问题的啦,但是,有时候,有能力和有时间有精力完全是不同的,哈哈。 最后插一句:其实 Nginx 的网络模型和事件模型是非常简单的( proxy ),只要对比一下 ngx lua 和 golang 两者的调度器和编程抽象就可以看出来了。 |
47
Ganing 2018-07-21 09:07:01 +08:00 via Android 1
点赞。从 LT 到 ET 事件驱动,再加上多线程锁,真的写起来太麻烦了。正在学习 goroutine 的原理,协程还是很有启发的
|
48
hnes OP @Ganing 十分感谢你的鼓励 :D
> 从 LT 到 ET 事件驱动,再加上多线程锁,真的写起来太麻烦了。正在学习 goroutine 的原理,协程还是很有启发的 是的,golang 中有很多很棒的思想可以在设计系统时让我们借鉴;但是,由于它的抽象层次过高,使得它不大适合某些对并发和资源消耗要求很高的领域,欢迎你继续关注 libaco 的后续开源项目哈 ;-) |
49
pynix 2018-07-21 13:55:23 +08:00 1
手动调度,坑多啊。。。
|
52
hnes OP 你好 @Monad,Github 上腾讯 libco 的 issue 90 中有十分详细的复现过程以及标准规范的引述,另外我在知乎上的一个回答中还陈述了腾讯 libco C++ ABI 相关的问题,感兴趣的话可以看一下哈 :D
tencent libco issue 90: https://github.com/Tencent/libco/issues/90 zhihu 回答: https://www.zhihu.com/question/52193579/answer/447612082 |
53
yulon 2018-07-23 08:55:30 +08:00 1
LZ 你好,我问你个可能很 safufu 的问题🤣,我看有些协程的实现中会用 pushfq/popfq 和 fnstenv/fldenv,这和单用 fnstcw/fldcw 有什么区别呢,因为加上 pushfq/popfq 后性能甚至不如 Windows Fiber 就有感而问。
|
54
hnes OP > LZ 你好,我问你个可能很 safufu 的问题🤣,我看有些协程的实现中会用 pushfq/popfq 和 fnstenv/fldenv,这和单用 fnstcw/fldcw 有什么区别呢,因为加上 pushfq/popfq 后性能甚至不如 Windows Fiber 就有感而问。
朋友客气了哈 :D 1. 关于 pushfq/popfq > The direction flag DF in the %rFLAGS register must be clear (set to “ forward ” direction) on function entry and return. Other user flags have no specified role in the standard calling sequence and are not preserved across calls. > Sys V ABI AMD64 Version 1.0 > The direction flag DF in the %EFLAGS register must be clear (set to “ forward ” direction) on function entry and return. Other user flags have no specified role in the standard calling sequence and are not preserved across calls. > Intel386-psABI-1.1 Sys V ABI 规范中规定(E|R)FLAGS 中的 status flags 都是"not preserved across calls",所以当我们进行遵从 Sys V ABI 规范的协程上下文切换时,根本不需要 save/restore 它们(比如 libaco 的 acosw 实现);另外,尽管 ABI 对(E|R)FLAGS 中的 DF 位有约束,但是我们可以证明在我们的协程上下文切换汇编中,它也是可以省掉的,证明很巧妙,具体可以阅读 libaco 的数学证明部分。 2. 关于 fnstenv/fldenv > The control bits of the MXCSR register are callee-saved (preserved across calls), while the status bits are caller-saved (not preserved). The x87 status word register is caller-saved, whereas the x87 control word is callee-saved. > Intel386-psABI-1.1 > The control bits of the MXCSR register are callee-saved (preserved across calls), while the status bits are caller-saved (not preserved). The x87 status word register is caller-saved, whereas the x87 control word is callee-saved. > Sys V ABI AMD64 Version 1.0 Sys V ABI 规范中规定对于 x87 与 MXCSR 只有 control words 是“ callee-saved (preserved across calls)”的,它们的 status 位全都是“ not preserved across calls ”,所以在 libaco 的协程上下文切换中,我们只需要 save/restore 它们的控制字就可以了( fnstcw/fldcw & stmxcsr/ldmxcsr ),当然使用 fnstenv/fldenv 也是正确的,但是: fnstcw/fldcw 只需要 save/restore 两个字节,而 fnstenv/fldenv 却需要 save/restore 28 个字节,这个 28 个字节中有 26 个字节是毫无必要的无用功。 (对于上述两部分的 call convention,Sys V ABI 与 Miscrosoft ABI 是基本相同的,故上面只引述了 Sys V ABI 的规范描述) Reference: https://www.felixcloutier.com/x86/FSTENV:FNSTENV.html https://www.felixcloutier.com/x86/FSTCW:FNSTCW.html https://github.com/hnes/libaco/blob/master/README_zh.md#proof-of-correctness > 加上 pushfq/popfq 后性能甚至不如 Windows Fiber 你是指如果没有 pushfq/popfq 的话,性能比 Windows Fiber 好很多,加上之后反而会比 Windows Fiber 慢很多么? 如果是上述陈述的话,我认为是很奇怪的,可以具体再确认一下,毕竟 pushfq/popfq 还是很快的( RFLAGS 8 个字节,但是还有可能是因为 pushfq/popfq 破坏了流水线而带来的性能损耗)。 但是如果本来性能与 Fiber 相等,加上它们之后更慢了,这就很正常的,毕竟这样就多了来回 16 个字节读写的无端损耗。 当然,我认为是不需要加的,除非还有其他的目的。 对于 windows 的协程,建议只用它提供的 API,比如 Fiber,原因是 windows 的 ABI 只有微软说了算...哈哈。 |
55
hnes OP @yulon
> Sys V ABI 规范中规定(E|R)FLAGS 中的 status flags 都是"not preserved across calls",所以当我们进行遵从 Sys V ABI 规范的协程上下文切换时,根本不需要 save/restore 它们(比如 libaco 的 acosw 实现);另外,尽管 ABI 对(E|R)FLAGS 中的 DF 位有约束,但是我们可以证明在我们的协程上下文切换汇编中,它也是可以省掉的,证明很巧妙,具体可以阅读 libaco 的数学证明部分。 中更正: Sys V ABI 规范中规定(E|R)FLAGS 中的 status flags {+(除了 DF 位)+}都是"not preserved across calls" |
57
yulon 2018-07-23 20:45:07 +08:00 1
@hnes 是我忽略了 fnstenv/fldenv 要占更多字节,其实主要是这货拖慢了效率,只加 pushfq/popfq 还是比 Fiber 快的🤣
|
59
toilaj 2018-07-24 08:51:50 +08:00 1
已 fork,学习中~~
|
61
myself659 2018-07-31 14:39:29 +08:00
文档很详细 下了很大功夫 赞
|
62
HeartJ 2018-09-09 22:14:26 +08:00
最近在学习协程,感谢分享和提供这么好的学习资源
|