C语言关于协程的探究

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

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

  • 协程是程序的资源,线程是操作系统的资源。协程是由程序自己创建和调度的,不需要操作系统的干预,因此协程的创建和切换的开销比线程小得多。线程是由操作系统创建和调度的,需要在用户态和内核态之间切换,因此线程的创建和切换的开销比协程大得多。
  • 协程是在一个线程中运行的,线程可以在多个 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(&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在生产者协程和消费者协程之间进行切换。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值