SysV信号量线程交替执行异常:线程永久阻塞问题排查
问题描述
我实现了一个类似Java和.NET的监视器类,相比互斥锁+条件变量的组合效率更高。该类使用两个信号量:一个用于进入已锁定的临界区,另一个用于通知等待慢竞争路径的线程,等待中的线程会同时等待这两个信号量。
在Win32平台使用独立信号量,Linux平台则用SysV信号量集模拟Win32的WaitForMultipleObjects(),同时等待监视器的“互斥锁”部分解锁和通知信号。
我编写了C++20测试代码,创建两个各含2个信号量的信号量集,初始化setA的两个信号量值为1,启动两个线程,每个线程先等待自己信号量集的两个信号量(执行减1操作),再对对方的信号量集的两个信号量执行加1操作。但测试中两个线程均卡在首次semop调用处,陷入永久休眠,请求排查代码错误。
附测试代码:
#include <thread> #include <initializer_list> #include <sys/types.h> #include <sys/sem.h> #include <sys/stat.h> using namespace std; int main() { int setA = semget( IPC_PRIVATE, 2, S_IRUSR | S_IWUSR ), setB = semget( IPC_PRIVATE, 2, S_IRUSR | S_IWUSR ); if( setA == -1 || setB == -1 ) return EXIT_FAILURE; static auto xSemop = []( int set, std::initializer_list<sembuf> sems ) { int ret; while( (ret = ::semop( set, const_cast<sembuf *>(sems.begin()), sems.size() )) == EINTR ); return ret; }; if( xSemop( setA, { { 0, 1, 0 } } ) || xSemop( setA, { { 1, 1, 0 } } ) ) return EXIT_FAILURE; constexpr size_t ROUNDS = 1'000'000; auto thr = []( int mySet, int yourSet ) { for( size_t r = ROUNDS; r--; ) if( xSemop( mySet, { { 0, -1, 0 }, { 1, -1, 0 } } ) || xSemop( yourSet, { { 0, 1, 0 }, { 1, 1, 0 } } ) ) terminate(); }; jthread thrA( thr, setA, setB ), thrB( thr, setB, setA ); }
错误原因分析
- setB信号量未初始化:SysV信号量集创建后,所有信号量初始值默认为0。代码仅初始化了setA的两个信号量为1,但setB的两个信号量保持0值。线程thrB启动后,尝试对setB的两个信号量执行减1操作,因信号量值不足直接阻塞。
- 线程执行链路阻塞:线程A首次
semop成功后,会给setB的信号量加1使其变为1,理论上能唤醒线程B,但如果测试环境中线程B先于线程A启动并进入阻塞,可能会让用户误以为两个线程都卡在首次调用;另外未初始化的setB会导致整个循环链路无法正常启动。
修复方案
- 初始化所有信号量:创建setB后,同步初始化其两个信号量为1,确保两个线程的初始执行条件都满足:
if( xSemop( setA, { { 0, 1, 0 } } ) || xSemop( setA, { { 1, 1, 0 } } ) || xSemop( setB, { { 0, 1, 0 } } ) || xSemop( setB, { { 1, 1, 0 } } ) ) return EXIT_FAILURE;
- 更规范的初始化方式(推荐):使用
semctl的SETVAL命令直接设置信号量初始值,避免依赖默认的0值:
// 部分系统需显式定义semun联合结构 union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; }; int main() { // ... 信号量集创建逻辑 ... semun init_val; init_val.val = 1; // 初始化setA和setB的所有信号量为1 if( semctl(setA, 0, SETVAL, init_val) == -1 || semctl(setA, 1, SETVAL, init_val) == -1 || semctl(setB, 0, SETVAL, init_val) == -1 || semctl(setB, 1, SETVAL, init_val) == -1 ) return EXIT_FAILURE; // ... 线程创建逻辑 ... }
- 验证执行逻辑:修复初始化后,两个线程将可以正常完成100万次循环,不会出现永久阻塞。
内容的提问来源于stack exchange,提问作者Edison von Myosotis
相关产品推荐
相关产品推荐

