問題
一段老代碼,兩個線程,一個線程調用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;
- 最好使用其他方式替代信號量,比如條件變量;