且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

协程 , C语言实现

更新时间:2021-07-19 05:04:01

协程 , 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的信号处
理函数其实是无法执行的,因为当前栈已经溢出了。

有趣的是,这同时也提供了我们一个获取堆栈的机会。也就是说,一个拥有独立运行栈空间的协程创建可以由下所示:

  1. 提供新的信号处理函数执行栈。
  2. 设置某个信号的执行函数,并暗示在新的堆栈执行
  3. 发送该信号并等待该信号处理执行结束

    1. 在信号执行函数中使用setjmp拿到该堆栈的上下文,占为己有。
  4. 恢复该信号的处理函数
  5. 恢复信号处理函数堆栈

    现在.我们可以通过longjmp的方式成功跳转到我们独自的堆栈。配合Duff's device,我们就可以实现一个比较完备的协程了。如果对信号的处理比较了解的同学,具体的实现就比较简单了。这里不过多涉及。

Thread

我最近才了解到某些语言使用线程来模拟协程,这我觉得很有意思,协程竟然有这样的魔力。这让我想起了在高中的逸闻趣事。陪同学去买奶茶,来一杯红豆奶茶,不要红豆.

后记

说了半天协程的实现,协程真的解决了任何程序上的性能问题么。我无意去宣扬这些实现协程的奇技淫巧,但是通过这些实现,我们可以清清楚楚,明明白白的看清楚协程是什么,它并不是什么包治百病的灵丹妙药,从机器的角度来说,这不过是一个高级一点的jmp指令而已。

如果有一天,你不得不用所谓的协程来减少线程切换的代价,那你可以问问自己,真的需要这么多的线程么。线程从来都不是自己产生的,产生线程的,是人。

现在的人大多习惯于从人的角度来设计开发,但有些时候,从机器的角度来看问题也会有一些不一样的收获。

C 协程实践 ----- 简单的回合制游戏

考虑一个简单的回合制游戏,主角A和NPC进行对战。我们假定游戏规则如下:

  1. 主角A主动发起攻击,接着NPC再进行攻击。依次循环,直到其中一人的血量低于0为止。
  2. 主角和NPC初始攻击伤害都为10.
  3. 主角和NPC都拥有闪避率和暴击率属性。
  4. 闪避成功则不受伤害。
  5. 一旦暴击,伤害翻倍。

一个协程的实现如下(这里我使用了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();
}