C语言自定义互斥算法中线程/核心间写操作未正确传播的原因排查
C语言自定义互斥算法中线程/核心间写操作未正确传播的原因排查
我来帮你拆解这个问题——你的互斥逻辑存在一个关键漏洞,导致偶尔会出现两个线程同时进入临界区的情况,进而让counter只被递增一次。咱们一步一步来分析:
1. 核心问题:互斥逻辑的「时间窗口」漏洞
先看线程t1的等待循环逻辑:
while (aload(&shared->f2, mo_acquire)) { astore(&shared->f1, false, mo_release); thrd_yield(); astore(&shared->f1, true, mo_release); }
当t1检测到t2已经设置f2=true时,它会先把自己的f1设为false、让出CPU,再重新把f1设为true。这个过程存在一个致命的时间窗口:
- 假设
t2此时正在执行自己的while (aload(&shared->f1, mo_acquire))循环,当t1把f1设为false的瞬间,t2的循环条件不再满足,会直接退出循环进入临界区执行shared->counter++。 - 而
t1在thrd_yield()之后重新设置f1=true,再次检查f2时,t2可能已经执行完临界区并把f2设为false了。此时t1的循环条件也不满足,会跟着进入临界区。 - 最终结果就是两个线程同时操作
counter,由于counter++是非原子操作,竞态条件会导致它只被实际递增一次。
2. 关于内存序的使用:你的用法是正确的,但不是问题根源
你用mo_release存储标志位、mo_acquire加载标志位的方式是符合C内存模型要求的:
mo_release语义确保:shared->counter++的所有副作用,都会在astore(&shared->f1, false, mo_release)完成前被提交,其他线程加载f1=false时能看到这些更新。mo_acquire语义确保:加载f2/f1时,会同步到其他线程之前用mo_release存储的所有操作的结果。
你观察到汇编里没有显式的lock前缀或内存栅栏是正常的——x86架构的CPU本身就保证了store的release语义和load的acquire语义(除了特定的写合并场景),所以编译器不需要生成额外的屏障指令。
3. 修复方案:改用Peterson算法的正确实现
你需要引入一个turn变量来消除时间窗口,这就是经典Peterson算法的核心逻辑。修改后的代码如下:
第一步:修改共享结构体,新增turn变量
typedef struct { int counter; atomic_bool f1; atomic_bool f2; atomic_int turn; // 新增:标记当前优先进入临界区的线程 } Shared;
第二步:初始化turn变量
在makeShared()函数中添加:
atomic_init(&shared->turn, 0);
第三步:修正线程逻辑
static int t1(Shared* const shared) { astore(&shared->f1, true, mo_release); astore(&shared->turn, 2, mo_release); // 声明:我愿意让t2优先进入 // 等待条件:t2想进入,且当前轮到t2 while (aload(&shared->f2, mo_acquire) && aload(&shared->turn, mo_acquire) == 2) { thrd_yield(); } // 临界区 shared->counter++; astore(&shared->f1, false, mo_release); return 0; } static int t2(Shared* const shared) { astore(&shared->f2, true, mo_release); astore(&shared->turn, 1, mo_release); // 声明:我愿意让t1优先进入 // 等待条件:t1想进入,且当前轮到t1 while (aload(&shared->f1, mo_acquire) && aload(&shared->turn, mo_acquire) == 1) { thrd_yield(); } // 临界区 shared->counter++; astore(&shared->f2, false, mo_release); return 0; }
Peterson算法通过turn变量明确指定“哪个线程优先”,彻底消除了两个线程同时进入临界区的时间窗口,保证了严格的互斥。
4. 额外建议
- 如果你只是学习用,这个实现没问题;但实际项目中,应该直接用标准库提供的互斥原语(比如
mtx_t),它们经过了严格的测试和优化。 - 可以用
tsan(ThreadSanitizer)来检测竞态条件,它能快速帮你定位这类线程同步问题,命令是:gcc -O3 -Wall -Wextra -fsanitize=thread your_code.c -o your_program。
内容来源于stack exchange
相关产品推荐
相关产品推荐

