Pthread与POSIX信号应用程序的异常行为排查
首先,我们来拆解你的问题核心:信号发送的时机恰好落在Worker线程的临界区之外,导致信号对value的修改被Worker线程的后续操作覆盖,而局部变量/全局变量的差异影响了临界区的执行时长,进而改变了信号命中临界区的概率。
关键逻辑梳理
先明确各个线程的核心行为:
- Worker线程:持有互斥锁时,先给
value累加0-9999的和,再减去同样的和,最终value回到原值;解锁后重复循环。 - Interrupter线程:每秒给Worker线程发一次信号10,信号处理函数会给
value加1。 - Supervisor线程:每秒加锁读取并打印
value,解锁后休眠1秒。
异常的根本原因
当i是局部变量时,GCC会把它分配到寄存器中(而非内存),这让Worker线程的两个for循环执行得极快,临界区(从加锁到解锁)的持续时间非常短。在128核的服务器上,线程调度异常频繁,Worker线程大部分时间处于临界区之外(解锁后到下一次加锁前的间隙)。
这时候,Interrupter发送的信号大概率命中Worker线程的临界区之外:
- 信号处理函数执行
value++,value从N变成N+1; - 紧接着Worker线程进入临界区,执行两个for循环:先给
value累加0-9999的和,再减去同样的和——相当于把value从N+1又改回了N; - Supervisor线程加锁读取时,看到的还是N,所以会连续打印同一个值。
只有当信号恰好命中Worker线程的临界区内(比如正在执行第一个for循环时),信号对value的修改才不会被覆盖:
- 信号打断第一个for循环,
value加1; - Worker线程继续完成第一个for循环(累加剩余的i值),然后执行第二个for循环减去全部i值;
- 因为两个for循环的累加/减去的总和相等,最终
value会保留信号加的1(从N变成N+1); - Supervisor线程读取到N+1,开始连续打印这个新值。
为什么改成全局i就正常?
当i是全局变量时,每次访问i都要读取内存(而非寄存器),Worker线程的for循环执行速度大幅变慢,临界区的持续时间显著变长。这时候,Interrupter发送的信号几乎都会命中Worker线程的临界区内,信号对value的修改不会被覆盖,所以Supervisor线程每次打印的都是递增的值,和你本地测试的结果一致。
为什么本地WSL2环境正常?
WSL2默认是虚拟化的少量核心(比如2核),线程调度远没有128核服务器频繁。Worker线程的临界区执行时间相对较长,信号大概率命中临界区内,所以不会出现value被覆盖的情况。
验证与修复思路
你可以通过以下方式验证这个结论:
- 在Worker线程的临界区末尾(解锁前)加入
usleep(100),人为拉长临界区时长,即使i是局部变量,异常也会消失; - 查看服务器上的编译优化等级(GCC默认是
-O0,但某些环境可能开启了更高优化),如果开启了-O2或以上,编译器会更激进地优化局部变量的循环,加剧这个问题。
修复方案的核心是确保信号对value的修改不会被Worker线程覆盖:
- 可以在信号处理函数中也加锁(注意:信号处理函数中应该使用可重入的互斥锁操作,比如
pthread_mutex_lock_np,避免死锁); - 调整Worker线程的逻辑,避免在临界区外让
value被修改后又恢复。
内容的提问来源于stack exchange,提问作者Sterpu Mihai

