sem_init重复调用引发sem_wait线程无法被唤醒
问题
一段老代码,两个线程,一个线程调用sem_wait等待信号量,另外一个线程在某失败分支会调用sem_init清信号量,结果导致sem_wait线程无法被唤醒;
分析
Linux manpage
从描述中可见,初始化一个已经被初始化的信号量会导致未定义行为;
1 NAME 2 sem_init - initialize an unnamed semaphore 3 4 SYNOPSIS 5 #include <semaphore.h> 6 7 int sem_init(sem_t *sem, int pshared, unsigned int value); 8 9 Link with -lrt or -pthread. 10 11 DESCRIPTION 12 ... 13 14 Initializing a semaphore that has already been initialized results in undefined behavior.
glibc源码
到底会发生什么未定义行为,我们直接看源码吧;
首先,对比结构体,旧结构体只有value成员,新结构体中增加了private和nwaiters成员;nwaiters成员会在调用sem_wait时候增加;
然后,对比sem_post唤醒函数;可见,新唤醒函数会在唤醒操作执行之前对nwaiters进行判断,只有当nwaiters>0时,才进行唤醒;
而旧的唤醒操作,则没有类似判断;
现在,我们清楚了,老代码用的老版本的glibc,内部没有等待判断,一直没有出问题,而使用新版本的glibc之后,加入了判断,就有问题了;
结论
- sem_init是用来在初始化的时候调用初始化信号量的,并不是用来将信号量清零的;
- 重复调用sem_init的行为可能导致已经处于sem_wait的线程无法被唤醒;
- 旧版本的glibc机制比较弱,所以老代码一直运行很好;但是新glibc作了检查,所以会出问题;
- 按照目前代码看,如果单个线程自己在调用了sem_wait之后再调用sem_init时没什么影响的;但是不保证以后的glibc会再做什么修改造成影响;
- 除了初始化阶段,其他流程中不要使用sem_init;
- 最好使用其他方式替代信号量,比如条件变量;