协作式调度器严禁使用while(true)或sleep_for等阻塞操作,必须将任务拆分为非阻塞状态机,通过时间戳比对实现延时,用枚举状态分步执行硬件操作,并采用双缓冲等单线程安全机制保障数据一致性。

协作式调度器里不能用 while(true) 或 sleep_for
协作式调度器依赖每个任务主动让出 CPU,一旦某个任务进入长循环或阻塞调用(比如 std::this_thread::sleep_for、read()、usleep()),整个轮转就卡死——后续所有任务都得不到执行机会。这不是“慢”,而是彻底停摆。
典型错误写法:
void long_task() {
while (data_not_ready) { // 卡在这里,调度器无法切走
std::this_thread::sleep_for(10ms); // OS 线程阻塞,协程/任务也挂死
}
process(data);
}
- 必须把耗时逻辑拆成“检查 → 返回 → 下次再查”的状态机形式
- 避免任何会触发内核阻塞的系统调用(
read、accept、pthread_cond_wait等) - 用非阻塞 I/O + 轮询标志位替代真实等待
用时间戳+状态变量实现可中断的延时逻辑
裸机或轻量级调度器没有内核定时器支持,所以不能靠“注册回调”来唤醒,得靠每次轮转时比对当前时间戳与目标时间戳是否满足条件。
示例:一个需延迟 500ms 后执行的子步骤
struct DelayedStep {
uint32_t start_ms = 0;
bool started = false;
bool done = false;
<pre class="brush:php;toolbar:false;">void run() {
if (!started) {
start_ms = get_tick_ms(); // 假设是 HAL_GetTick() 或类似
started = true;
return;
}
if (get_tick_ms() - start_ms >= 500) {
do_actual_work();
done = true;
}
}
};
-
get_tick_ms()必须是单调递增、无 wrap-around 风险的(或做防溢出处理) - 状态字段(
started、done)要定义在任务对象内,不能靠局部变量保存 - 如果任务需要多次分步执行(如发送一帧 SPI 数据分 4 次写寄存器),就把每一步编码为枚举状态
如何安全地拆分阻塞型硬件操作
像 OLED 刷新、ADC 连续采样这类操作,表面看是“一次调用等结果”,实际底层往往包含多阶段:配置寄存器 → 等待 BUSY 标志 → 读数据 → 清中断。这些阶段都可以拆开,中间插入 return 让出控制权。
以 SPI 写屏为例:
enum class OledState { IDLE, CMD_SENT, DATA_SENT, DONE };
OledState state = OledState::IDLE;
<p>void oled_task() {
switch (state) {
case OledState::IDLE:
spi_write_cmd(0xAF); // 发送开启显示命令
state = OledState::CMD_SENT;
return;
case OledState::CMD_SENT:
if (!spi_is_busy()) { // 非阻塞查询
spi_write_data(frame_buffer, len);
state = OledState::DATA_SENT;
}
return;
case OledState::DATA_SENT:
if (!spi_is_busy()) state = OledState::DONE;
return;
default: break;
}
}</p>
- 所有硬件访问函数必须是非阻塞的(
spi_write_xxx只推数据进 FIFO,不等传输完成) - 状态机跳转必须显式控制,不能靠隐式顺序执行
- 注意外设状态寄存器读取的时效性——有些芯片要求连续读两次才能更新标志位
共享数据同步要避开锁和原子操作
协作式调度下不存在真正并行,所有任务都在同一线程上下文顺序执行,所以不需要 std::mutex 或 std::atomic 来保护共享变量——但必须防止“未完成写入就被读取”这种中间态问题。
- 用“双缓冲 + 标志位”代替临界区:新数据先写入 buffer_b,写完再原子切换指针(即使只是普通赋值,在单线程中也是安全的)
- 避免结构体成员部分更新:比如
sensor_data.temp和sensor_data.humid分两次赋值,中间被其他任务读到就会拿到脏数据 - 若需跨任务传递复杂状态,推荐用
enum class显式表达有效状态,而不是靠布尔组合判断
最易被忽略的一点:状态拆分不是为了“看起来没卡”,而是确保每个 run() 调用的执行时间稳定可控——否则哪怕平均只花 20μs,某次因缓存失效或分支预测失败飙到 200μs,就可能错过传感器采样窗口或通信超时。这在资源紧张的单片机上尤为致命。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











