协程 , C语言实现
有意思的是,这并不是一篇科普文。
什么是协程
按照惯例,本文应该讲一讲协程是什么,而不是着急开始高谈论阔实现的细枝末节,何况其实协程的实现并不怎么复杂。但讲着协程是什么的文章太多了,有一语中的的,也有以讹传讹,更有胡编乱造的。一语中的的,高手如云,自不必多说;以讹传讹的,虽有不妥,但也不是什么大恶;剩下的,胡编乱造,作惊人之语,不看也就罢了。
不过协程到底是什么呢?一些人把协程视作轻量级的线程,一些人把协程看作是回调的变体。前者的确是有失偏颇。后者我倒找不出什么错漏的地方,毕竟协程的确绝大多数都可以用回调改写,但从我自己的理解来看,协程也可以被看成是能够保存上下文,可让出执行但不可抢占的执行单元。在操作系统的早期,就是使用的协程来模拟并发,后来被可抢占的线程所代替,这也是认为轻量级线程这种说法有失偏颇的原因了。
无论如何,不管是回调也好,不可抢占的执行单元也罢,从本质上来说,不会对性能有产生多大帮助。很有意思的是,如果我说协程能够大幅提高程序运行效率,保不定有人相信;如果我说回调能大幅提高程序运行效率,必然会有人笑我愚蠢。朝三而暮四,朝四而暮三,有什么不同呢。
Duff's device
Duff's device 指的是在C语言中一种交织switch和其他控制语句来手动展开循环的能力。一个
典型的 Duff's device如下文所示。
switch(i){
case 1:
if(j == 0){
break;
}
case 2:
if( b == 1){
case 3:
}
从本质来说,Duff's device更像是一种语法糖。当然,这看上去似乎没有用处,但这却是在C
语言中实现协程最直接最简单的技术了。一个简单的协程伪代码如下所示:
static int fd = 0;
void jobs(){
static int i = 0;
switch(i){
case 0:
addr = get_addr();
fd = connect(addr); // 建立连接
i = 1;
return ;
case 1:
data,len = read(fd);
printf("%.*s",len,data); // 读数据
i = 2;
return ;
case 2:
len = write(fd,data); // 写数据
i ++;
return ;
}
}
tnet_worker[0].call = jobs;
tnet_worker[0].resume = jobs;
select(nfds,rfds,wrds,NULL,NULL){
if(FD_ISSET(sigfd,rfds)){
tnet_worker[0].call();
add2rfds(fd);
}
if(FD_ISSET(fd,rfds)){
tnet_worker[0].resume();
}
if(FD_ISSET(fd,wrds)){
tnet_worker[0].resume();
}
}
这基本就是一个简易协程的雏形,这可能跟一些人想象中的协程大不相同,但是很可惜,这就是所谓的协程。
用static用来保存上下文,return用来让出逻辑上不可抢占的执行函数。这已经涵盖了协程的关键要素了。
更有意思的是,如果你愿意,大可使用 do-connect,do-read,do-write 之类的形式的函数回调取代它们。它没有让我们性能变的更好,也没有让我们性能变得更差,但是的确让我们的代码逻辑变得更清楚了。
至此,我们可以创建成千上万tnet_worker来处理每一条连接,这可能像线程,但是我们都已经知道,这跟线程完全不一样。
Duff's device 固然很不错,但是有个很严重的问题,局部变量无法跨函数保存。虽然我可以创建成千上万个
协程,但是他们却是在共用一个堆栈。从上文也可以看出,我使用的是一个static局部变量保存协程的上下文,这对per协程per入口来说没有问题,对于多协程单入口就有些力不从心了。Simon Tatham 在 https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html 一文中提供了一个基于Duff's device的C代码宏
,同时也包装了一个对于单入口下,上下文的保存问题。但是本质上还是使用的static局部变量。这就导致使用这个宏需要作出较多的假设,这里不做深入分析。(btw,我非常喜欢这个宏,的确可以让代码逻辑关系变得更清楚,尤其是对于C代码)。
基于这个考量,我们需要给每一个协程分配独自的栈空间,另一方面我们需要在不同堆栈中相互跳转的能力,这超出了传统的控制语句的能力范围。前者,我们还需要好好讨论一下,后者很简单,熟悉C语言的同学都知道,标准C提供了
[sig]longjmp/setjmp系列函数,提供了保存上下文跨函数跳转的能力。这个很简单,因此就不做深入研究了。
ucontext
我先说三件很有意思的事实:
- 绝大多数协程实现使用ucontext
- ucontext 是被废弃的函数,在最新的POSXI标准中已经无法找到该函数。
-
POSIX建议使用pthread作为一种可能的替代方式。
ucontext天生就有独自的栈空间,也天生提供相互跳转。所以实现起来,这的确比较简单,不过考虑改类函数已经被废弃了,我也不做过多的介绍了。
sigaltstack
__sigaltstack -- set and/or get signal stack context__
从名字上就可以看出,这是一个跟信号有关的函数。它的本意其实是使用用户指定的堆栈来执 行信号处理函数。一个典型的使用场景可以考虑在栈溢出的场景,所产生的SIGSEGV的信号处 理函数其实是无法执行的,因为当前栈已经溢出了。
有趣的是,这同时也提供了我们一个获取堆栈的机会。也就是说,一个拥有独立运行栈空间的协程创建可以由下所示:
- 提供新的信号处理函数执行栈。
- 设置某个信号的执行函数,并暗示在新的堆栈执行
-
发送该信号并等待该信号处理执行结束
- 在信号执行函数中使用setjmp拿到该堆栈的上下文,占为己有。
- 恢复该信号的处理函数
- 恢复信号处理函数堆栈
现在.我们可以通过longjmp的方式成功跳转到我们独自的堆栈。配合Duff's device,我们就可以实现一个比较完备的协程了。如果对信号的处理比较了解的同学,具体的实现就比较简单了。这里不过多涉及。
Thread
我最近才了解到某些语言使用线程来模拟协程,这我觉得很有意思,协程竟然有这样的魔力。这让我想起了在高中的逸闻趣事。陪同学去买奶茶,来一杯红豆奶茶,不要红豆.
后记
说了半天协程的实现,协程真的解决了任何程序上的性能问题么。我无意去宣扬这些实现协程的奇技淫巧,但是通过这些实现,我们可以清清楚楚,明明白白的看清楚协程是什么,它并不是什么包治百病的灵丹妙药,从机器的角度来说,这不过是一个高级一点的jmp指令而已。
如果有一天,你不得不用所谓的协程来减少线程切换的代价,那你可以问问自己,真的需要这么多的线程么。线程从来都不是自己产生的,产生线程的,是人。
现在的人大多习惯于从人的角度来设计开发,但有些时候,从机器的角度来看问题也会有一些不一样的收获。
C 协程实践 ----- 简单的回合制游戏
考虑一个简单的回合制游戏,主角A和NPC进行对战。我们假定游戏规则如下:
- 主角A主动发起攻击,接着NPC再进行攻击。依次循环,直到其中一人的血量低于0为止。
- 主角和NPC初始攻击伤害都为10.
- 主角和NPC都拥有闪避率和暴击率属性。
- 闪避成功则不受伤害。
- 一旦暴击,伤害翻倍。
一个协程的实现如下(这里我使用了pcl库):
#include "pcl.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
struct attacker_st{
uint64_t damage ;
int64_t HP ;
coroutine_t coro ;
uint8_t critical_rate; // percent
uint8_t dodge_rate;
};
static struct attacker_st attacker_buda = {
.HP = 100,
.critical_rate = 80,
.dodge_rate = 50,
};
static struct attacker_st attacker_duzi = {
.HP = 100,
.critical_rate = 20,
.dodge_rate = 20,
};
uint64_t compute_damage(uint8_t critical_rate)
{
uint32_t r = rand() % 100 + 1;
if(critical_rate > r){
return (20);
}
return (10);
}
int dodge(uint8_t dodge_rate)
{
uint32_t r = rand() % 100 + 1;
if(dodge_rate > r){
return (1);
}
return (0);
}
void Attacker_BUDA(void *args)
{
while(attacker_buda.HP > 0){
attacker_buda.damage = compute_damage(attacker_buda.critical_rate);
co_call(attacker_duzi.coro); // yeild
if(attacker_duzi.HP <= 0){ // duzi is GG,break the loop
break;
}
if(!dodge(attacker_buda.dodge_rate)){
attacker_buda.HP -= attacker_duzi.damage;
}
}
}
void Attacker_DUZI(void *args)
{
while(1){
if(!dodge(attacker_duzi.dodge_rate)){
attacker_duzi.HP -= attacker_buda.damage;
}
if(attacker_duzi.HP > 0){
attacker_duzi.damage = compute_damage(attacker_duzi.critical_rate);
}
co_resume();
}
}
int main()
{
uint8_t i ;
srandom((long int) &i);
co_thread_init();
attacker_buda.coro = co_create(Attacker_BUDA,NULL,0,32768);
attacker_duzi.coro = co_create(Attacker_DUZI,NULL,0,32768);
co_call(attacker_buda.coro);
if(attacker_buda.HP >= attacker_duzi.HP){
printf("the Winner is Buda\n");
}else {
printf("Shit,Duzi Win\n");
}
co_thread_cleanup();
}