协程是一种用户级的轻量级线程,它可以在单线程里多个函数并发地执行协程,可以在主任务进行的同时,执行一些分支任务,以协助程序达到最终的效果,我们可以将协程成为用户态线程,但它与线程又有所区别。
协程和线程是两种不同的并发编程的概念,它们都可以用来执行多个任务,但是它们之间有一些区别,主要有以下几点:
- 协程是程序的资源,线程是操作系统的资源。协程是由程序自己创建和调度的,不需要操作系统的干预,因此协程的创建和切换的开销比线程小得多。线程是由操作系统创建和调度的,需要在用户态和内核态之间切换,因此线程的创建和切换的开销比协程大得多。
- 协程是在一个线程中运行的,线程可以在多个 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; // 协程ID
void* 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; // 协程ID
switch(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(¬_full); // 等待缓冲区不满
buffer[in] = i; // 写入数据到缓冲区
printf("produce %d\n", i++);
in = (in + 1) % BUFFER_SIZE; // 更新写入位置
count++; // 更新有效数据数量
sem_post(¬_empty); // 通知缓冲区不空
yield(); // 切换到消费者协程
}
}
// 消费者协程
void consumer(void* arg) {
int i = 0;
while (1) {
sem_wait(¬_empty); // 等待缓冲区不空
i = buffer[out]; // 读取数据从缓冲区
printf("consume %d\n", i);
out = (out + 1) % BUFFER_SIZE; // 更新读取位置
count--; // 更新有效数据数量
sem_post(¬_full); // 通知缓冲区不满
yield(); // 切换到生产者协程
}
}
int main() {
// 初始化信号量
sem_init(¬_full, BUFFER_SIZE);
sem_init(¬_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在生产者协程和消费者协程之间进行切换。