协程是一种用户级的轻量级线程,它可以在单线程里多个函数并发地执行协程,可以在主任务进行的同时,执行一些分支任务,以协助程序达到最终的效果,我们可以将协程成为用户态线程,但它与线程又有所区别。

协程和线程是两种不同的并发编程的概念,它们都可以用来执行多个任务,但是它们之间有一些区别,主要有以下几点:

  • 协程是程序的资源,线程是操作系统的资源。协程是由程序自己创建和调度的,不需要操作系统的干预,因此协程的创建和切换的开销比线程小得多。线程是由操作系统创建和调度的,需要在用户态和内核态之间切换,因此线程的创建和切换的开销比协程大得多。
  • 协程是在一个线程中运行的,线程可以在多个 CPU 上运行。协程是在一个线程的上下文中执行的,它们共享同一个栈空间和内存空间,因此协程之间的数据交换和同步比较容易。线程是在多个 CPU 上并行执行的,它们拥有自己的栈空间和内存空间,因此线程之间的数据交换和同步比较复杂。
  • 协程是为了提高并发性能,线程是为了提高并行性能。协程适合处理 IO 密集型的任务,它们可以在 IO 阻塞时切换到其他协程继续执行,从而提高 CPU 的利用率。线程适合处理 CPU 密集型的任务,它们可以利用多核 CPU 的优势来加速计算。

C语言本身不支持协程,但是可以通过一些库或者技巧来实现协程的功能。

1. 使用setjmp和longjmp实现保存和恢复执行环境。setjmp可以保存堆栈环境,longjmp可以恢复该环境继续执行。

2. 使用函数指针和上下文结构体(context struct)保存执行流信息。通过函数指针调用来切换执行流。

3. 使用类似select机制的函数来管理多个协程,当某个协程可以继续运行时切换至其执行流。这里给出一种简单的协程实现:

#include <stdio.h>
#include <stdlib.h>// 协程上下文
typedef struct Context {int id;              // 协程IDvoid* stack;         // 协程栈void (*func)(void*); // 指向协程函数的函数指针  void *arg;           // 协程函数参数
} Context;// 协程调度器
void scheduler();// 当前运行的协程
Context* currCtx = NULL;  // 协程函数
void co1(void *arg) {printf("coroutine 1\n");scheduler();
}void co2(void *arg) {printf("coroutine 2\n"); scheduler();
}// 协程调度器
void scheduler() {static int cid = 0;    // 协程IDswitch(cid) {case 0:currCtx = (Context*)malloc(sizeof(Context));currCtx->id = cid++;currCtx->func = co1;currCtx->arg = NULL; break; case 1:free(currCtx->stack);currCtx->stack = NULL;currCtx = (Context*)malloc(sizeof(Context));  currCtx->id = cid++;currCtx->func = co2;    currCtx->arg = NULL;  break;}// 执行当前协程currCtx->func(currCtx->arg);
}int main() {scheduler();return 0;
}

这个程序定义了一个上下文结构体Context表示一个协程,scheduler函数作为协程调度器,它会根据当前的cid来选择并执行一个协程函数。

主要流程是:

1. 程序开始会执行scheduler,cid为0,所以创建co1协程并执行。

2. co1执行完后再次调用scheduler,此时cid为1,所以创建co2协程并执行。

3. co2执行完后程序结束。这样,通过使用函数指针和上下文结构体保存各协程的执行信息,并在scheduler中进行切换,实现了简单的协程执行效果。

我们还可以通过setjmp和longjmp实现保存和恢复执行环境的特性来实现C语言的协程。优化完善协程代码如下

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>// 协程上下文
typedef struct Context {jmp_buf env;         // 协程环境void* stack;         // 协程栈void (*func)(void*); // 指向协程函数的函数指针void *arg;           // 协程函数参数 struct Context* prev; // 链表指针,用于实现双向链表struct Context* next; // 链表指针,用于实现双向链表
} Context;// 当前线程的协程数组
static thread_local Context** ctxArray = NULL; // 当前线程当前运行协程在数组中的位置
static thread_local int currPos = 0;// 当前线程协程数量
static thread_local int ctxCount = 0;// 当前线程协程链表头结点
static thread_local Context* ctxHead = NULL;// 当前线程协程链表尾结点
static thread_local Context* ctxTail = NULL;// 协程栈大小
#define STACK_SIZE 1024// 协程栈池大小
#define STACK_POOL_SIZE 10// 协程栈池
static thread_local void* stackPool[STACK_POOL_SIZE];// 协程栈池中可用栈的数量
static thread_local int stackCount = 0;// 创建一个新的协程
void create(void (*func)(void*), void* arg); // 切换到下一个协程
void yield();// 销毁当前协程
void destroy();  // 协程函数
void co1(void *arg) {printf("coroutine 1\n"); yield();
}void co2(void *arg) {printf("coroutine 2\n");yield();
}// 创建协程
void create(void (*func)(void*), void* arg) {// 如果数组为空,就分配内存并初始化if (ctxArray == NULL) {ctxArray = malloc(sizeof(Context*) * STACK_POOL_SIZE);for (int i = 0; i < STACK_POOL_SIZE; i++) {ctxArray[i] = NULL;}}// 如果数组已满,就扩容两倍if (ctxCount == STACK_POOL_SIZE) {int newSize = STACK
int newSize = STACK_POOL_SIZE * 2;Context** newArray = malloc(sizeof(Context*) * newSize);for (int i = 0; i < newSize; i++) {if (i < STACK_POOL_SIZE) {newArray[i] = ctxArray[i];} else {newArray[i] = NULL;}}free(ctxArray);ctxArray = newArray;}// 创建一个新的协程上下文Context* ctx = malloc(sizeof(Context));ctx->func = func;ctx->arg = arg;ctx->prev = NULL;ctx->next = NULL;// 从协程栈池中获取一个可用的栈空间,如果没有就分配一个新的if (stackCount > 0) {ctx->stack = stackPool[--stackCount];} else {ctx->stack = malloc(STACK_SIZE);}// 将新的协程上下文加入到数组和链表中ctxArray[ctxCount++] = ctx;if (ctxHead == NULL) {ctxHead = ctxTail = ctx;} else {ctxTail->next = ctx;ctx->prev = ctxTail;ctxTail = ctx;}
}// 切换到下一个协程
void yield() {// 如果没有协程,就直接返回if (ctxCount == 0) {return;}// 保存当前协程的上下文if (setjmp(ctxArray[currPos]->env) == 0) {// 切换到下一个协程的位置currPos = (currPos + 1) % ctxCount;// 恢复下一个协程的上下文longjmp(ctxArray[currPos]->env, 1);} else {// 如果是从其他协程切换过来,就执行当前协程的函数Context* currCtx = ctxArray[currPos];currCtx->func(currCtx->arg);// 执行完毕后,销毁当前协程destroy();}
}// 销毁当前协程
void destroy() {// 如果没有协程,就直接返回if (ctxCount == 0) {return;}// 获取当前协程的上下文Context* currCtx = ctxArray[currPos];// 将其栈空间回收到协程栈池中,如果池已满,就释放掉if (stackCount < STACK_POOL_SIZE) {stackPool[stackCount++] = currCtx->stack;} else {free(currCtx->stack);}// 将其从数组和链表中移除,并释放其内存for (int i = currPos; i < ctxCount - 1; i++) {ctxArray[i] = ctxArray[i + 1];}if (currCtx == ctxHead) {ctxHead = currCtx->next;if (ctxHead != NULL) {ctxHead->prev = NULL;}} else if (currCtx == ctxTail) {ctxTail = currCtx->prev;if (ctxTail != NULL) {ctxTail->next = NULL;}} else {currCtx->prev->next = currCtx->next;currCtx->next->prev = currCtx->prev;}free(currCtx);// 减少协程数量ctxCount--;
}

这个协程实现的关键点有:

1. 使用数组和双向链表结合管理多个协程上下文,实现高效的创建、销毁和查询。

2. 使用线程本地存储避免多线程并发问题。

3. 使用协程栈池管理栈空间,避免每次创建协程都分配和释放栈。

4. 在销毁协程时将其上下文从数组和链表中移除,并回收其栈空间,节省内存。

5. 使用内联函数定义简单易用的API。

6. 采用抢占式协程调度,协程函数无需显式切换。

7. 数组使用动态增长避免固定大小的限制

基于此协程我们可以写一个简单的使用生产着-消费者场景

#include <stdio.h>
#include <stdlib.h>
#include "coroutine.h"  // 协程实现头文件// 生产者协程
void producer(void* arg) {int i = 0;while (1) {printf("produce %d\n", i++);yield();  // 切换到消费者协程}
}// 消费者协程
void consumer(void* arg) {int i = 0;while (1) {printf("consume %d\n", i++);yield();  // 切换到生产者协程}
}int main() {// 创建生产者和消费者协程create(producer, NULL);create(consumer, NULL);while (1) {// 重要:主while循环必须调用yield切换协程// 否则协程将无法运行yield();}
}

这段代码中,定义了两个协程函数(producer和consumer),它们分别负责生成和处理整数数据,并打印出来。然后在main函数中,创建了两个协程,并在一个while循环中不断地切换协程。这样可以实现生产者和消费者之间的交替执行,形成一种简单的同步机制。

我们可以进一步优化这个生产着-消费者

#include <stdio.h>
#include <stdlib.h>
#include "coroutine.h"  // 协程实现头文件
#include "semaphore.h"  // 信号量实现头文件// 缓冲区大小
#define BUFFER_SIZE 10// 缓冲区
int buffer[BUFFER_SIZE];// 缓冲区中有效数据的数量
int count = 0;// 缓冲区中可写入数据的位置
int in = 0;// 缓冲区中可读取数据的位置
int out = 0;// 缓冲区不满的信号量
sem_t not_full;// 缓冲区不空的信号量
sem_t not_empty;// 生产者协程
void producer(void* arg) {int i = 0;while (1) {sem_wait(&not_full); // 等待缓冲区不满buffer[in] = i;      // 写入数据到缓冲区printf("produce %d\n", i++);in = (in + 1) % BUFFER_SIZE; // 更新写入位置count++;                     // 更新有效数据数量sem_post(&not_empty); // 通知缓冲区不空yield();              // 切换到消费者协程}
}// 消费者协程
void consumer(void* arg) {int i = 0;while (1) {sem_wait(&not_empty); // 等待缓冲区不空i = buffer[out];      // 读取数据从缓冲区printf("consume %d\n", i);out = (out + 1) % BUFFER_SIZE; // 更新读取位置count--;                       // 更新有效数据数量sem_post(&not_full); // 通知缓冲区不满yield();             // 切换到生产者协程}
}int main() {// 初始化信号量sem_init(&not_full, BUFFER_SIZE);sem_init(&not_empty, 0);// 创建生产者和消费者协程create(producer, NULL);create(consumer, NULL);while (1) {// 重要:主while循环必须调用yield切换协程// 否则协程将无法运行yield();}
}

这个示例使用协程和信号量实现了生产者-消费者模型。

主要流程是:

1. 创建固定大小的缓冲区用于传递数据。

2. 初始化not_full信号量的值为缓冲区大小,not_empty信号量的值为0。

3. 创建生产者协程和消费者协程。

4. 生产者协程使用not_full信号量等待缓冲区可写入,写入数据后释放not_empty信号量。

5. 消费者协程使用not_empty信号量等待缓冲区可读取,读取数据后释放not_full信号量。

6. 主循环使用yield在生产者协程和消费者协程之间进行切换。

C语言关于协程的探究相关推荐

  1. [通用技术]在不同语言中用协程实现全排列算法(C++/Lua/Python/C#)

    我这里实现全排列的基本算法如下(C++): 1 #include <algorithm> 2 #include <iostream> 3 #include <vector ...

  2. go 怎么等待所有的协程完成_GO语言基础进阶教程:Go语言的协程——Goroutine

    Go语言的协程--Goroutine 进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程) 进程进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为&qu ...

  3. Go语言中协程的概念和基本使用

    为什么80%的码农都做不了架构师?>>>    Go协程(Goroutine)是与其他函数同时运行的函数.可以认为Go协程是轻量级的线程.与创建线程相比,创建Go协程的成本很小.因此 ...

  4. 浅尝Go语言的协程实现

    文章目录 为什么需要协程 协程的本质 协程如何在线程中执行 GMP调度模型 协程并发 为什么需要协程 协程的本质是将一段数据的运行状态进行打包,可以在线程之间调度,所以协程就是在单线程的环境下实现的应 ...

  5. Go语言入门到实战——14.Go语言的协程机制以及并发机制

    Go语言入门到实战--00主目录 在上一讲中我们学习了Go语言的包的知识已经依赖管理. 协程(groutine)是一种更加轻量级的线程(thread). 一.协程与线程得到比较 1.对于java而言, ...

  6. C语言中协程(coroutine)实现

    C语言协程库实现说明 代码实现 1. 当前支持的功能概览 1.1 创建任意数量协程并在协程中yield #include <stdio.h> #include <stdlib.h&g ...

  7. Unity 协程原理探究与实现

    目录 一.介绍 二.迭代器 三.原理 四.总结 一.介绍 协程Coroutine在Unity中一直扮演者重要的角色.可以实现简单的计时器.将耗时的操作拆分成几个步骤分散在每一帧去运行等等,用起来很是方 ...

  8. C语言实现协程----初探

    协程定义 协程,从编程的角度看来,可以理解为用户自己能控制和调度的线程,可以理解为用户级别的线程.一个线程可以有多个协程,一个进程也可以有多个协程. 协程实现 协程的实现有多种方式 setjmp和lo ...

  9. go语言ants协程池

    参考链接:大数据Ants go ants学习 比如多文件处理,常用 package patternhandleimport ("fmt""sync"" ...

最新文章

  1. Oracl 12c (课本)
  2. [Win32]IP数据报的首部如何定义
  3. request.getParameter和request.getAttribute之间的区别
  4. Java8 - 一文搞定Fork/Join 框架
  5. 关于Jboss/Tomcat/Jetty的JNDI定义123
  6. MMDetection-数据准备
  7. Linu解决oracle连接不稳定
  8. 基于JAVA+SpringMVC+Mybatis+MYSQL的旅游管理系统
  9. android 回退函数,android浏览器研究-回退和前进
  10. docker RUN、CMD 和 ENTRYPOINT
  11. 小技巧:如何在 js 中使用 apply 语法执行 new?
  12. 190704每日一句
  13. 解析ES6箭头函数中的this
  14. 基于FPGA的数字信号处理3.7开平方运算分析
  15. org.apache.jasper.JasperException: /app/jsonp.jsp(1,2) Page-encoding specified in jsp-property-grou
  16. OpenGL ES 2.0 系列一
  17. vue 在线阅读PDF
  18. bailian.openjudge 1190:生日蛋糕
  19. Hog源码注释(hog.cpp的注解)
  20. 进程管理工具-Supervisord +CeSi

热门文章

  1. 2018和2019年经济学人The Economist词频统计
  2. 猪之歌qq表情包 2004 是什么
  3. 怎样使用PDF阅读器浏览查看PDF文档
  4. Android聊天室背景,安卓实现类似QQ刚换聊天背景的功能
  5. matlab解决迷宫问题,用matlab处理蚂蚁迷宫问题
  6. CAJ文件怎么转换成Word文档?这2种方法值得学习
  7. Java 知网博硕论文信息爬虫
  8. 【记录】戴尔台式机预装WIN10降WIN7
  9. 平价好用蓝牙耳机推荐,买了不亏高性价比耳机系列
  10. “私车公用”如何扣除?且看五省市税局回复