C语言中实现协程案例

 更新时间:2021年07月02日 16:42:23   作者:adinosaur  
这篇文章主要介绍了C语言中实现协程案例,本文通过将协程与线程和异步回调进行对比,以及具体实现案例,以下就是详细内容,需要的朋友可以参考下

协程是一种用户空间的非抢占式线程,主要用来解决等待大量的IO操作的问题。

协程vs线程

对比使用多线程来解决IO阻塞任务,使用协程的好处是不用加锁,访问共享的数据不用进行同步操作。这里需要说明的一点是,使用协程之所以不需要加锁不是因为所有的协程只在一个线程中运行,而是因为协程的非抢占式的特点。也就是说,使用协程的话,在没主动交出CPU之前都是不会被突然切换到其它协程上的。而线程是抢占式的,使用多线程你是不能确定你的线程什么时候被操作系统调度,什么时候被切换,因此需要用锁到实现一种“原子操作”的语义。

协程vs异步回调

其实更一般更常见的做法是,使用非阻塞的IO(比如是异步IO,又或者是在syscall上自己实现的一套异步IO,如asio)并且将处理操作写在回调函数中。这样的做法一般没什么问题,但当回调函数变多,一段连贯的业务代码就会被拆分到多个回调函数之中,增加维护的成本。因此使用协程可以用同步的写法写出效果相当于是异步的代码。

利用static变量实现协程

要实现一个协程,主要的问题是如何保存函数调用的上下文。之前在网上看到一篇博客coroutines in c,用一种非常简洁的方式实现了这个保存上下文的功能。实现代码如下:

#define crBegin static int _cr_state = 0; switch(_cr_state) { case 0:
#define crReturn(x) do { _cr_state = __LINE__; return x; case __LINE__:; } while (0)
#define crFinish }

int func1() {
    crBegin
    while (1)
    {
        printf("hello world\n");
        crReturn(0);
    }
    crFinish
}

这个代码利用了函数的static变量来保存函数调用状态。注意,由于vs2013有一个调试特性,所以vs2013的__LINE__的实现不是常量因此会编译不通过,使用gcc就可以编译。这段代码简单是简单但是有问题,比如说如果两个协程调用同一个函数,就会出错。因此博客里面提及这段代码主要是给出一个思路,如果实际使用的话这样子肯定是不行的。

利用setjmp、longjmp实现协程

前面说过,实现协程最主要的是保存函数的调用的上下文,而这些上下文主要就两个部分:1.各个寄存器的值,2.函数调用栈。C语言里可以通过setjmp来保存函数调用时,各寄存器的值。保存之后,便可以通过longjmp重现回到当初setjmp的地方(可以理解成跨函数的goto)。但是,需要注意的是,setjmp仅负责保存寄存器的值,不负责维护其函数调用栈(这个看看setjmp的jmp_buf的结构就知道了),因此必须由使用者来手动的维护这个函数调用栈。使用setjmp、longjmp的一个常见的错误就是,尝试去longjmp到一个已经执行完的函数,这时候虽然寄存器的值是当时保存的值,但是调用栈已经不是原来的调用栈了。

而我的做法是,在创建一个协程的时候在堆上申请一块空间(大小为2M)作为协程的调用栈,然后在setjmp的时候,手动更改寄存器esp的值,使其指向这个我自己创建的调用栈。因此在以后运行的时候,这个协程就会使用我提供的那块内存作为栈。

我的这个协程库提供了三个接口:

  1. coro_new:创建一个协程
  2. coro_yield:将控制权返回给调度协程
  3. coro_main:运行调度协程

协程的控制流程如下:

  1. 通过coro_main运行调度协程,并找出下一个运行的协程,运行之。
  2. 运行这个协程直到其调用coro_yield将控制权返还给调度协程。
  3. 重复以上两个步骤,直到所有协程运行完毕。

这个协程库实现的非常简单,只有100来行的代码,当然实现它的目的是为了提供一个最简单的协程模型,而不是一个功能完整、鲁棒性强的能投入实际业务运行的协程。

因此问题还是有很多的:

  1. 比如当在协程里面调用栈超过2M时,这个是需要处理的,现在的代码是没有做的,理应中断程序,避免写坏堆,产生随机的不可重现的问题。
  2. 显然在实现时没有考虑到多线程,如果在多线程环境里面运行,需要代码做同步处理。
  3. 现在的这个版本的协程有一个约定,在协程里调用的函数不能阻塞在syscall,这显然也是不科学的。一个完整的协程库,应该包含一些常用的syscall的非阻塞的实现,毕竟只有一个线程不能真的阻塞在这个调用上。

总结

当然实现协程还有比较一些更好的方法,比如如果能用glibc的ucontext库就可以基于这个库来实现,而不用自己手动管理函数调用的上下文了,如云风实现的协程库。

到此这篇关于C语言中实现协程案例的文章就介绍到这了,更多相关C语言实现协程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++超详细分析讲解内联函数

    C++超详细分析讲解内联函数

    为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数
    2022-06-06
  • C语言UDP传输系统源码

    C语言UDP传输系统源码

    这篇文章主要为大家分享了C语言UDP传输系统的源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • C++中结构体和Json字符串互转的问题详解

    C++中结构体和Json字符串互转的问题详解

    这篇文章主要给大家介绍了关于C++中结构体和Json字符串互转问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • C++类和对象到底是什么

    C++类和对象到底是什么

    C++ 是一门面向对象的编程语言,理解 C++,首先要理解类(Class)和对象(Object)这两个概念。下面和小编一起来学习吧
    2021-09-09
  • 浅谈C语言共用体和与结构体的区别

    浅谈C语言共用体和与结构体的区别

    下面小编就为大家带来一篇浅谈C语言共用体和与结构体的区别。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-02-02
  • 使用C语言编写钢琴小程序

    使用C语言编写钢琴小程序

    这篇文章主要为大家详细介绍了使用C语言编写钢琴小程序,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • VS2019配置OpenCV时找不到Microsoft.Cpp.x64.user的解决方法

    VS2019配置OpenCV时找不到Microsoft.Cpp.x64.user的解决方法

    这篇文章主要介绍了VS2019配置OpenCV时找不到Microsoft.Cpp.x64.user的解决方法,需要的朋友可以参考下
    2020-02-02
  • CentOS下Jsoncpp安装配置的方法

    CentOS下Jsoncpp安装配置的方法

    本文主要介绍了CentOS下Jsoncpp安装配置的方法,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • C++生成key-value键值的三种方式总结

    C++生成key-value键值的三种方式总结

    这篇文章主要为大家详细介绍了C++中生成key-value键值的三种方式,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解下
    2023-09-09
  • C++面向行输入之get()与getline()实例详解

    C++面向行输入之get()与getline()实例详解

    在c++里当我们输入一个字符串时习惯用cin,但是cin只能读取一段不含空格的字符串,如果我们需要读取一段包含空格的字符串时,就需要用到getline()或get(),下面这篇文章主要给大家介绍了关于C++面向行输入之get()与getline()的相关资料,需要的朋友可以参考下
    2021-10-10

最新评论