一、协程初探:为什么需要协程?

各位同学,咱们今天聊个有意思的话题——协程。

说实话,我刚开始做嵌入式开发那会儿,根本不知道协程是什么。那时候项目里全是裸机轮询加中断,后来上了RTOS,开始用线程。直到有一次做一个多路传感器采集的项目,8个传感器要同时采集、处理、上报,线程切换的开销让我头疼不已。嗯,就是那次,我开始认真研究协程。

1.1 一个真实的痛点

想象一下这个场景:你写了一个程序,要同时做三件事——从串口读数据、处理数据、把结果发到网络。用传统方式怎么写?

// 伪代码:传统轮询方式
while(1) {
    read_serial();      // 可能阻塞
    process_data();     // 耗时操作
    send_network();     // 可能阻塞
}

这代码有个致命问题:每一步都可能卡住。串口没数据?等着。网络拥堵?等着。整个程序就卡死了。

用线程呢?

// 伪代码:多线程方式
thread1: while(1) { read_serial(); }
thread2: while(1) { process_data(); }
thread3: while(1) { send_network(); }

线程能解决问题,但代价不小。每个线程有自己的栈空间,少则几KB,多则几十KB。8个传感器?8个线程?光栈空间就吃掉几百KB。在资源受限的MCU上,这简直是奢侈。

核心问题:我们需要一种轻量级的并发方案,既能像线程一样独立执行,又不像线程那样消耗资源。

1.2 协程是什么?

协程,说白了就是一个可以暂停、可以恢复的函数。

你想想看,普通函数一旦调用,必须从头执行到尾。协程不一样——它可以在中间某个点停下来,保存现场,然后下次调用时从停下来的地方继续执行。

这个特性,让协程成了「用户态的轻量级线程」。

二、协程与线程、进程的区别

我经常被问到这个问题:「协程和线程到底有啥区别?」

咱们用一张表说清楚:

对比维度 进程 线程 协程
调度单位 操作系统 操作系统 用户程序
切换开销 高(上下文切换、页表切换) 中(内核态切换) 低(用户态切换,几纳秒)
栈空间 独立地址空间,大 独立栈,通常几KB~MB 共享栈或小栈,几百字节
并发数量 几十个 几百个 成千上万个
数据共享 IPC机制 共享内存(需加锁) 直接共享(无需锁)
抢占性 抢占式 抢占式 协作式(主动让出)

看到区别了吗?协程最大的优势就是轻量。它不需要内核参与调度,完全由程序自己控制。切换时只需要保存几个寄存器和栈指针,开销极小。

我的经验:在STM32F103这种资源有限的MCU上,我曾经用协程同时管理了32个传感器通道。如果用线程,光栈空间就超过256KB——而这块芯片总共只有64KB RAM。协程帮我省下了90%的内存。

三、C语言实现协程的挑战

好了,既然协程这么好,为什么C语言里不常见?

原因很简单:C语言没有原生的协程支持。

你看Python有async/await,Go有goroutine,C++20也有了co_await。但C语言?标准库连个协程的影子都没有。

为什么?因为C语言太「底层」了。它没有保留函数调用栈的状态,没有闭包,没有生成器。要实现协程,你得自己搞定三件事:

3.1 保存和恢复执行状态

协程的核心能力是「暂停后恢复」。在C语言里,这意味着你要手动保存:

  • 程序计数器(PC)——下次从哪执行
  • 栈指针(SP)——局部变量在哪
  • 寄存器状态——计算到哪了

我早期尝试用setjmp/longjmp来实现,但很快就踩坑了——longjmp不能跨函数跳转,而且局部变量可能被优化掉。嗯,这里要注意,编译器优化经常让setjmp的行为变得不可预测。

3.2 独立的栈空间

每个协程需要自己的栈。但C语言的函数调用栈是共享的。你想想看,如果两个协程共用同一个栈,A协程调用了一个函数,B协程切进来,A的栈帧就被覆盖了。

解决方案有两种:

  • 独立栈:每个协程分配独立的栈内存,切换时切换栈指针
  • 共享栈:协程暂停时把栈内容拷贝到堆上,恢复时再拷贝回来

我在项目中用的是独立栈方案。虽然内存占用稍大,但实现简单,性能也更好。

3.3 调度器的设计

有了协程,还得有个调度器来管理它们。调度器要解决:

  • 什么时候切换协程?
  • 切换到哪个协程?
  • 如何处理协程的创建、销毁、等待?

最简单的调度器就是「轮询」——挨个执行每个协程,直到它们主动让出CPU。但实际项目中,我们往往需要更复杂的调度策略,比如优先级调度、事件驱动调度。

避坑指南:我曾经在调度器里用了全局变量来保存当前协程状态,结果在多文件编译时出现了符号冲突。后来我改用结构体封装,每个协程实例独立管理状态,问题才解决。

四、知识体系总览

下面这张图,是我梳理的协程知识体系。你可以把它当作整个课程的地图:

C语言协程知识体系 协程核心 为什么需要协程? 与线程/进程对比 C语言实现挑战 轻量级并发需求 资源受限场景 避免回调地狱 调度单位不同 切换开销对比 内存占用对比 保存/恢复执行状态 独立栈空间管理 调度器设计 掌握协程 = 掌握高效并发的钥匙

五、写在前面的话

说实话,C语言实现协程确实有挑战。但正因为有挑战,才值得我们去攻克。

我见过太多嵌入式工程师,遇到并发问题就上RTOS,结果资源不够用。也见过一些人用状态机硬扛,代码写得像意大利面条。协程,恰恰是这两者之间的「黄金平衡点」。

接下来的课程,我会带你一步步从零实现一个完整的C语言协程库。我们会从最基础的setjmp/longjmp开始,然后实现协程的创建、切换、调度,最后做一个实际的项目案例。

准备好了吗?咱们开始动手。


公众号:蓝海资料掘金营,微信deep3321