Sven

协程


常见的协程

  • Python:通过asyncio库和async/await语法支持协程。
  • JavaScript:通过Promiseasync/await支持协程。
  • Go:通过goroutinechannel实现协程。
  • Kotlin:通过kotlinx.coroutines库支持协程。
  • C#:通过async/await支持协程。
  • Lua:原生支持协程。

C的协程

1. ucontext(user context)

还可以使用setjmp/longjmp

getcontext:获取当前执行上下文。

setcontext:切换到指定的上下文。

makecontext:创建一个新的上下文。

swapcontext:保存当前上下文并切换到另一个上下文。

ucontext_t:表示上下文的结构体,包含寄存器状态、栈信息等。

ucontext demo:

#include <stdio.h>
#include <stdlib.h>
#include <ucontext.h>

ucontext_t main_context, co_context;

void coroutine_function() {
    printf("Coroutine started\n");
    swapcontext(&co_context, &main_context);  // 切换回主上下文
    printf("Coroutine resumed\n");
}

int main() {
    // 为协程分配一个栈, 
    // 设置该上下文执行时的栈,以便在函数执行时可以正确地保存和恢复局部变量和返回地址。
    char stack[8192];

    // 获取当前上下文
    getcontext(&co_context);
    
    // 设置协程的栈和入口函数
    co_context.uc_link = &main_context;  // 结束后返回到 main_context
    co_context.uc_stack.ss_sp = stack; // ss: stack space, sp: stack pointer
    co_context.uc_stack.ss_size = sizeof(stack);
    
    // 创建协程并设置其入口点
    makecontext(&co_context, coroutine_function, 0);

    // 切换到协程上下文执行
    swapcontext(&main_context, &co_context);

    printf("Back to main\n");
    return 0;
}

另一个比较复杂的demo(c):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>

#define MAX_COROUTINES 128  // 最多支持的协程数
#define STACK_SIZE 65536    // 64KB 栈空间

// 协程上下文
typedef struct coroutine {
    ucontext_t context;      // 协程的上下文
    void (*func)(void *);    // 协程的执行函数
    void *arg;               // 协程函数的参数
    char *stack;             // 协程的栈
    int finished;            // 标记协程是否执行完毕
} coroutine_t;

coroutine_t *coroutines[MAX_COROUTINES];  // 存储所有协程
int coroutine_count = 0;                  // 当前协程数量
int current_coroutine = -1;               // 当前正在执行的协程索引

ucontext_t main_context;  // 主程序的上下文

int create_coroutine(void (*func)(void *), void *arg) {
    if (coroutine_count >= MAX_COROUTINES) {
        return -1;  // 超过最大协程数量
    }

    coroutine_t *co = (coroutine_t *)malloc(sizeof(coroutine_t));
    if (!co) return -1;

    co->func = func;
    co->arg = arg;
    co->stack = (char *)malloc(STACK_SIZE);  // 为协程分配栈空间
    if (!co->stack) {
        free(co);
        return -1;
    }

    // 初始化协程的上下文
    if (getcontext(&co->context) == -1) {
        free(co->stack);
        free(co);
        return -1;
    }

    // 设置协程的栈和入口函数
    co->context.uc_stack.ss_sp = co->stack;
    co->context.uc_stack.ss_size = STACK_SIZE;
    co->context.uc_link = &main_context;  // 协程结束后返回到主上下文

    // 设置协程的入口函数
    makecontext(&co->context, (void (*)(void))func, 1, arg);

    co->finished = 0;  // 初始化协程状态为未结束

    coroutines[coroutine_count] = co;
    return ++coroutine_count;
}

void yield() {
    if (coroutine_count == 0) return;

    int prev_coroutine = current_coroutine;
    current_coroutine = (current_coroutine + 1) % coroutine_count;

    // 检查当前协程是否已经结束
    if (prev_coroutine != -1 && coroutines[prev_coroutine]->finished) {
        // 移除已结束的协程
        free(coroutines[prev_coroutine]->stack);
        free(coroutines[prev_coroutine]);
        coroutines[prev_coroutine] = NULL;

        // 移动数组中的协程
        for (int i = prev_coroutine; i < coroutine_count - 1; ++i) {
            coroutines[i] = coroutines[i + 1];
        }
        coroutine_count--;
        current_coroutine--;  // 调整当前协程索引
        prev_coroutine--;
    }
    
    if (coroutine_count == 0) return;  // 所有协程已结束

    // 保存当前协程状态并切换到下一个协程
    if (prev_coroutine != -1) {
        swapcontext(&coroutines[prev_coroutine]->context, &coroutines[current_coroutine]->context);
    } else {
        // 第一次启动协程,直接切换
        setcontext(&coroutines[current_coroutine]->context);
    }
}

void example_coroutine(void *arg) {
    int i = *(int *)arg;
    printf("Coroutine %d started\n", i);
    for (int j = 0; j < 3; ++j) {
        printf("Coroutine %d, step %d\n", i, j);
        yield();  // 主动让出控制权
    }
    printf("Coroutine %d finished\n", i);

    // 标记协程为已结束
    coroutines[current_coroutine]->finished = 1;
    yield();  // 让出控制权,触发协程移除逻辑
}

int main() {
    // 创建多个协程
    int arg1 = 1, arg2 = 2, arg3 = 3;
    create_coroutine(example_coroutine, &arg1);
    create_coroutine(example_coroutine, &arg2);
    create_coroutine(example_coroutine, &arg3);

    // 保存主程序的上下文
    getcontext(&main_context);

    // 调度协程
    while (coroutine_count > 0) {
        yield();  // 切换到下一个协程
    }

    printf("All coroutines finished\n");

    return 0;
}

JS的无栈协程(Stackless Coroutine)

无栈协程的核心思想是:

  • 不分配独立栈:协程没有自己的栈空间,而是共享调用方的栈。
  • 状态机:通过状态机(State Machine)记录协程的执行状态,实现暂停和恢复。
  • 轻量级:由于没有独立的栈,无栈协程的创建和切换开销较小。

1. 无栈协程的实现原理

无栈协程的实现通常包括以下步骤:

将协程转换为状态机

  • 编译器或运行时将协程函数转换为一个状态机。
  • 每个 awaityield 点对应状态机的一个状态。
  • 协程的局部变量被提升为状态机的成员变量。
async/await 语法
  • async 函数标记一个函数为协程。
  • await 表达式暂停协程的执行,等待异步操作完成。

示例(JavaScript):

async function coroutine() {
    console.log("Start");
    await Promise.resolve(1);
    console.log("Resume");
    await Promise.resolve(2);
    console.log("End");
}

coroutine();
状态机转换
  • 编译器将协程函数转换为一个状态机。
  • 每个 awaityield 点对应状态机的一个状态。
// 原始协程函数
async function coroutine() {
    let x = 1;
    await delay(100);
    x += 1;
    await delay(100);
    return x;
}

// 转换为状态机
class CoroutineStateMachine {
    int state = 0;
    int x;

    void resume() {
        switch (state) {
            case 0:
                x = 1;
                state = 1;
                delay(100, this);  // 暂停并等待 delay 完成
                break;
            case 1:
                x += 1;
                state = 2;
                delay(100, this);  // 暂停并等待 delay 完成
                break;
            case 2:
                return x;  // 完成
        }
    }
}

保存和恢复状态

  • 当协程暂停时,保存当前状态(如程序计数器、局部变量等)。
  • 当协程恢复时,根据保存的状态跳转到对应的代码位置继续执行。

依赖事件循环

  • 无栈协程通常与事件循环(Event Loop)结合使用。
  • 协程暂停时,控制权返回给事件循环,继续执行其他任务。
  • 当异步操作完成时,事件循环会重新调度协程。

2. 无栈协程的实现技术

无栈协程的实现通常依赖于以下技术:

生成器(Generator)

  • 生成器是一种特殊的函数,可以通过 yield 暂停执行并返回一个值。
  • 生成器的状态(如局部变量、程序计数器)由运行时自动保存和恢复。

示例 (js):

在 JavaScript 中,生成器(Generator)是一种特殊的函数,可以通过 yield 关键字暂停执行并返回一个值;

function* 是 JavaScript 中定义生成器函数(Generator Function)的语法。生成器函数是一种特殊的函数,它可以通过 yield 关键字暂停执行并返回一个值,后续可以从暂停的地方恢复执行。生成器函数返回一个生成器对象(Generator Object),该对象符合可迭代协议(Iterable Protocol)和迭代器协议(Iterator Protocol)。


生成器函数的基本语法

生成器函数使用 function* 关键字定义,函数体内可以使用 yield 关键字暂停执行并返回一个值。

示例:

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}
  • function*:定义生成器函数。
  • yield:暂停函数执行并返回一个值。

生成器对象

生成器函数调用后不会立即执行,而是返回一个生成器对象。生成器对象具有以下方法:

  • next():恢复执行生成器函数,直到遇到下一个 yieldreturn
  • return():终止生成器函数的执行。
  • throw():在生成器函数内部抛出一个错误。

示例:

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const gen = myGenerator(); // 返回生成器对象

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
  • valueyieldreturn 返回的值。
  • done:表示生成器函数是否执行完毕(true 表示完成)。

生成器的特性

1. 惰性求值

生成器函数是惰性求值的,只有在调用 next() 时才会执行。

2. 可迭代

生成器对象符合可迭代协议,可以使用 for...of 循环或扩展运算符(...)遍历。

示例:

function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

for (const value of myGenerator()) {
    console.log(value); // 输出:1, 2, 3
}

const values = [...myGenerator()];
console.log(values); // 输出:[1, 2, 3]
3. 双向通信

生成器函数可以通过 yield 接收外部传入的值。

示例:

function* myGenerator() {
    const x = yield 1;
    const y = yield x + 2;
    return y;
}

const gen = myGenerator();

console.log(gen.next());    // { value: 1, done: false }
console.log(gen.next(10));  // { value: 12, done: false } (x = 10)
console.log(gen.next(20));  // { value: 20, done: true }  (y = 20)
4. 错误处理

生成器函数可以通过 throw() 方法抛出错误,并在函数内部使用 try...catch 捕获。

示例:

function* myGenerator() {
    try {
        yield 1;
        yield 2;
    } catch (error) {
        console.log("Caught error:", error);
    }
}

const gen = myGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error("Something went wrong"))); // 捕获错误并继续

生成器的应用场景

  • 惰性求值:处理大量数据时,按需生成值。
  • 异步编程:与 Promise 结合,实现类似 async/await 的功能。
  • 无限序列:生成无限序列(如斐波那契数列)。
  • 状态机:实现复杂的状态逻辑。

生成器与 async/await 的关系

async/await 是生成器的一种语法糖,底层实现类似于生成器 + Promiseasync 函数可以看作是一个自动执行的生成器函数。

示例:

// 使用生成器模拟 async/await
function* asyncGenerator() {
    const result = yield Promise.resolve(1);
    console.log(result); // 输出:1
}

function runAsync(generator) {
    const gen = generator();

    function handle(result) {
        if (result.done) return;
        result.value.then((data) => {
            handle(gen.next(data));
        });
    }

    handle(gen.next());
}

runAsync(asyncGenerator);

总结

  • function*:定义生成器函数。
  • yield:暂停函数执行并返回一个值。
  • 生成器对象:具有 next()return()throw() 方法。
  • 应用场景:惰性求值、异步编程、无限序列、状态机等。

生成器是 JavaScript 中一种强大的工具,可以用于实现复杂的控制流和异步编程逻辑。

另外python也有类似的生成器, 示例 (Python):

def coroutine():
    print("Start")
    yield 1
    print("Resume")
    yield 2
    print("End")

gen = coroutine()
print(next(gen))  # 输出:Start, 1
print(next(gen))  # 输出:Resume, 2

3. 无栈协程的优缺点

优点:

  • 轻量级:没有独立的栈,创建和切换开销小。
  • 简单易用:通过 async/awaityield 语法,简化异步编程。
  • 适合 I/O 密集型任务:与事件循环结合,高效处理异步 I/O。

缺点:

  • 无法在嵌套函数中暂停:只能在顶层函数中使用 awaityield
  • 依赖事件循环:需要与事件循环结合使用,无法直接用于 CPU 密集型任务。
  • 调试复杂性:异步代码的调试可能比同步代码更复杂。

4. 无栈协程的应用场景

  • 异步 I/O:如网络请求、文件读写。
  • 事件驱动编程:如 GUI 应用、游戏引擎。
  • 高并发服务器:如 Web 服务器、实时通信系统。

5. 总结

无栈协程通过状态机的方式实现暂停和恢复,不需要独立的栈空间,因此具有轻量级和高效率的特点。它的实现通常依赖于编译器或运行时的支持,如生成器、async/await 语法和状态机转换。无栈协程适合处理 I/O 密集型任务和事件驱动编程,但在复杂并发控制和 CPU 密集型任务中可能存在局限性


go的协程

是的,Go 语言的 goroutine 是有栈协程(Stackful Coroutine)。每个 goroutine 都有自己的栈空间,可以在任意嵌套函数中暂停和恢复。以下是 Go 有栈协程的详细说明:


1. 什么是有栈协程?

有栈协程(Stackful Coroutine)是指每个协程都有自己独立的栈空间,可以在任意嵌套函数中暂停和恢复。它的特点包括:

  • 独立的栈:每个协程都有自己的栈,可以保存函数调用的上下文。
  • 灵活性:可以在任意函数调用层次中暂停和恢复。
  • 上下文切换:通过保存和恢复寄存器状态实现上下文切换。

相比之下,无栈协程(Stackless Coroutine)没有独立的栈,只能在顶层函数中暂停和恢复(如 Python 的生成器)。


2. Go 的 goroutine 是有栈协程

Go 语言的 goroutine 是典型的有栈协程实现,具有以下特点:

  • 独立的栈:每个 goroutine 在创建时会被分配一个初始栈(默认大小为 2KB),栈空间会根据需要动态增长和收缩。
  • 轻量级:goroutine 的栈初始大小较小,且由 Go 运行时管理,创建和切换的开销远小于操作系统线程。
  • 调度器:Go 运行时包含一个高效的调度器(Scheduler),负责在多个操作系统线程上调度 goroutine。

3. goroutine 的栈管理

Go 的 goroutine 栈管理是其高效并发的重要基础:

  • 初始栈大小:每个 goroutine 的初始栈大小为 2KB。
  • 栈的动态增长:当 goroutine 的栈空间不足时,Go 运行时会自动分配更大的栈,并将原有栈内容复制到新栈中。
  • 栈的收缩:当 goroutine 的栈空间使用率较低时,Go 运行时会自动收缩栈,释放多余的内存。

这种动态栈管理机制使得 goroutine 既轻量又灵活,能够高效地支持大量并发任务。


4. goroutine 的上下文切换

goroutine 的上下文切换由 Go 运行时调度器负责,具体过程包括:

  1. 保存当前上下文:保存当前 goroutine 的寄存器状态和栈指针。
  2. 切换到目标上下文:恢复目标 goroutine 的寄存器状态和栈指针。
  3. 调度策略:Go 调度器基于工作窃取(Work Stealing)算法,确保负载均衡。

上下文切换的触发条件包括:

  • 系统调用:当 goroutine 执行阻塞的系统调用时,调度器会将其从当前线程分离,并调度其他 goroutine 执行。
  • channel 操作:当 goroutine 因 channel 操作阻塞时,调度器会切换到其他可运行的 goroutine。
  • 时间片耗尽:Go 调度器会为每个 goroutine 分配时间片,当时间片耗尽时触发切换。

5. goroutine 的优势

  • 高并发:可以轻松创建数十万甚至数百万个 goroutine。
  • 低开销:goroutine 的创建和切换开销远小于线程。
  • 简单易用:通过 go 关键字即可创建 goroutine,无需手动管理栈或上下文。

6. 与其他语言的对比

  • Python(无栈协程)
    • Python 的协程(如 asyncio)是无栈协程,依赖于生成器(Generator)实现。
    • 只能在顶层函数中暂停和恢复,灵活性较低。
  • C++(有栈协程)
    • C++20 引入了协程支持,但需要手动管理栈和上下文。
    • 实现复杂,不如 goroutine 易用。
  • Erlang(轻量级进程)
    • Erlang 的进程类似于 goroutine,但有独立的堆和栈。
    • 调度器与 Go 类似,但语法和生态系统不同。

7. 总结

Go 语言的 goroutine 是有栈协程的典型实现,具有独立的栈空间和高效的调度机制。它的设计使得 Go 能够轻松支持高并发编程,同时保持代码的简洁性和可读性。goroutine 的动态栈管理和轻量级上下文切换是其高效并发的重要基础。