一、协程初探:为什么需要协程?
各位同学,咱们今天聊个有意思的话题——协程。
说实话,我刚开始做嵌入式开发那会儿,根本不知道协程是什么。那时候项目里全是裸机轮询加中断,后来上了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语言实现协程确实有挑战。但正因为有挑战,才值得我们去攻克。
我见过太多嵌入式工程师,遇到并发问题就上RTOS,结果资源不够用。也见过一些人用状态机硬扛,代码写得像意大利面条。协程,恰恰是这两者之间的「黄金平衡点」。
接下来的课程,我会带你一步步从零实现一个完整的C语言协程库。我们会从最基础的setjmp/longjmp开始,然后实现协程的创建、切换、调度,最后做一个实际的项目案例。
准备好了吗?咱们开始动手。