如何避免get()与set()间的竞态条件?附代码场景分析
你已经精准抓住了问题核心:ActTask中存在检查-然后-执行的竞态窗口——在读取foo到写入修改后的值之间,ResetTask可能抢先修改foo,导致最终写入的是基于旧值的无效计算结果。咱们来逐一分析你提到的方案,并给出更可靠的解决思路:
现有方案的局限性
修改后的ActTask(重新读取foo后计算)
if (foocopy > 0) { x = AccessPeripheral1(); setfoo( getfoo() - x); }这个方案确实降低了竞态发生的概率,但本质上仍有漏洞:调用
getfoo()和setfoo()之间依然可能被ResetTask抢占,导致getfoo()获取的值在写入前又被修改,最终结果依然不符合预期。全局临界区方案
把ActTask或ResetTask的逻辑整体包裹进临界区,确实能解决竞态,但正如你担心的,嵌套临界区会大幅提升代码复杂度,不仅容易引发死锁,还会扩大临界区范围、降低系统并发性能,这个方案确实应该尽量避免。
最优解决方案:封装原子化的foo操作
核心思路是把“读取foo→检查条件→修改foo”的整个逻辑封装成一个原子操作,避免让任务直接处理锁的细节,同时最小化临界区范围。
方案1:基于互斥锁的原子操作封装
扩展foo的操作接口,新增一个专门用于“当foo>0时减去delta”的原子函数:
// 新增原子更新函数 bool updateFooSubtract(int32_t delta) { MutexLock(fooMutex); bool operationSuccess = false; if (foo > 0) { foo -= delta; operationSuccess = true; } MutexUnlock(fooMutex); return operationSuccess; }
然后修改ActTask,直接调用这个封装好的函数:
void ActTask() { while(1) { int32_t x = AccessPeripheral1(); // 无需自己处理锁,直接调用原子更新接口 updateFooSubtract(x); wait(actPeriod); } }
ResetTask保持原有逻辑即可,因为setfoo()本身已经是原子操作,且AccessPeripheral2()有自己的临界区,不要把它放进foo的锁里(避免嵌套锁风险):
void ResetTask() { while(1) { setfoo(resetvalue); AccessPeripheral2(); // 独立临界区,与foo的锁分离 wait(resetPeriod); } }
这个方案的优势:
- 临界区范围极小,只包含foo的检查和修改,对并发性能影响低
- 所有对foo的修改逻辑都封装在接口中,避免任务直接操作锁,降低出错概率
- 彻底消除了竞态条件,因为整个“检查+修改”操作是原子完成的
方案2:无锁原子操作(基于CAS)
如果你的平台支持C11标准的原子操作(或对应编译器的扩展),可以用**比较交换(CAS)**实现无锁的原子更新,避免互斥锁的开销和死锁风险:
#include <stdatomic.h> // 把foo改为原子变量 _Atomic int32_t foo; // 原子读取函数 int32_t getfoo(void) { return atomic_load(&foo); } // 原子写入函数 void setfoo(int32_t value) { atomic_store(&foo, value); } // 无锁原子更新:当foo>0时减去delta bool updateFooSubtract(int32_t delta) { int32_t currentVal = atomic_load(&foo); while (currentVal > 0) { int32_t newVal = currentVal - delta; // 如果当前foo的值还是currentVal,就更新为newVal;否则重试 if (atomic_compare_exchange_weak(&foo, ¤tVal, newVal)) { return true; } // 若更新失败,currentVal会被自动更新为foo的最新值,进入下一轮循环 } return false; }
ActTask和ResetTask的修改方式和方案1一致。这个方案适合对性能要求较高的场景,没有锁的开销,但需要注意CAS操作可能会有重试(当foo频繁被修改时),不过在你的业务场景中,重试次数通常会很少。
总结
- 避免将大段无关逻辑(如调用
AccessPeripheralX())放进foo的临界区,防止嵌套锁和临界区过大的问题 - 优先选择封装原子操作接口的方式,把锁或无锁逻辑隐藏在底层接口中,上层任务只需要调用接口即可
- 尽量不要让上层任务直接处理锁的细节,减少人为出错的可能
内容的提问来源于stack exchange,提问作者calofr

