APR分析-線程同步篇
在線程同步方面,Posix標(biāo)準(zhǔn)定義了3種同步模型,分別為互斥量、條件變量和讀寫鎖。APR也“淺”封裝了這3種模型,只是在“讀寫鎖”一塊兒還沒有全部完成。
線程同步的源代碼的位置在$(APR_HOME)/locks目錄下,本篇blog著重分析unix子目錄下的thread_mutex.c、thread_rwlock.c和thread_cond.c文件的內(nèi)容,其相應(yīng)頭文件為(APR_HOME)/include/apr_thread_mutex.h、apr_thread_rwlock.h和apr_thread_cond.h。
由于APR的封裝過于“淺顯”,實(shí)際上也并沒有多少值得分析的“靚點(diǎn)”。所以本篇實(shí)際上是在討論線程同步的3種運(yùn)行模型。
一、互斥量
互斥量是線程同步中最基本的同步方式。互斥量用于保護(hù)代碼中的臨界區(qū),以保證在任一時(shí)刻只有一個(gè)線程或進(jìn)程訪問臨界區(qū)。
1、互斥量的初始化
在POSIX Thread中提供兩種互斥量的初始化方式,如下:
(1)靜態(tài)初始化
互斥量首先是一個(gè)變量,Pthread提供預(yù)定義的值來支持互斥量的靜態(tài)初始化。舉例如下:
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
既然是靜態(tài)初始化,那么必然要求上面的mutex變量需要靜態(tài)分配。在APR中并不支持apr_thread_mutex_t的使用預(yù)定值的靜態(tài)初始化(但可以變通的利用下面的方式進(jìn)行靜態(tài)分配的mutex的初始化)。
(2)動(dòng)態(tài)初始化
除了上面的情況,如果mutex變量在堆上或在共享內(nèi)存中分配的話,我們就需要調(diào)用一個(gè)初始化函數(shù)來動(dòng)態(tài)初始化該變量了。在Pthread中的對(duì)應(yīng)接口為pthread_mutex_init。APR封裝了這一接口,我們可以使用下面方式在APR中初始化一個(gè)apr_thread_mutex_t變量。
apr_thread_mutex_t?*mutex?= NULL;
apr_pool_t??*pool?= NULL;
apr_status_t??stat;
stat =apr_pool_create(&pool, NULL);
if (stat !=APR_SUCCESS) {
printf("error in pool %d/n", stat);
} else {
printf("ok in pool/n");
}
stat =apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, pool);
if (stat !=APR_SUCCESS) {
printf("error %d in mutex/n", stat);
} else {
printf("ok in mutex/n");
}
2、互斥鎖的軟弱性所在
互斥鎖之軟弱性在于其是一種協(xié)作性鎖,其運(yùn)作時(shí)對(duì)各線程有一定的要求,即“所有要訪問臨界區(qū)的線程必須首先獲取這個(gè)互斥鎖,離開臨界區(qū)后釋放該鎖”,一旦某一線程不遵循該要求,那么這個(gè)互斥鎖就形同虛設(shè)了。如下面的例子:
舉例:我們有兩個(gè)線程,一個(gè)線程A遵循要求,每次訪問臨界區(qū)均先獲取鎖,然后將臨界區(qū)的變量x按偶數(shù)值遞增,另一個(gè)線程B不遵循要求直接修改x值,這樣即使在線程A獲取鎖的情況下仍能修改臨界區(qū)的變量x。
static apr_thread_mutex_t??????*mutex? = NULL;
staticint???????????????????????????????x?????? = 0;
staticapr_thread_t????????????*t1???? = NULL;
staticapr_thread_t????????????*t2???? = NULL;
static void * APR_THREAD_FUNC thread_func1(apr_thread_t *thd, void*data)
{
apr_time_t????? now;
apr_time_exp_t?xt;
while (1) {
apr_thread_mutex_lock(mutex);
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadA]: own the lock, time[%02d:%02d:%02d]/n", xt.tm_hour,xt.tm_min,
xt.tm_sec);
printf("[threadA]: x = %d/n", x);
if (x % 2 || x == 0) {
x += 2;
} else {
printf("[threadA]: Warning: x變量值被破壞,現(xiàn)重新修正之/n");
x += 1;
}
apr_thread_mutex_unlock(mutex);
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadA]: release the lock, time[%02d:%02d:%02d]/n",xt.tm_hour, xt.tm_min,
xt.tm_sec);
sleep(2);
}
return NULL;
}
static void * APR_THREAD_FUNC thread_func2(apr_thread_t *thd, void*data)
{
apr_time_t????? now;
apr_time_exp_t?xt;
while (1) {
x ++;
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadB]: modify the var, time[%02d:%02d:%02d]/n",xt.tm_hour, xt.tm_min,? xt.tm_sec);
sleep(2);
}
return NULL;
}
int main(int argc, const char * const * argv, const char * const*env)
{
apr_app_initialize(&argc, &argv, &env);
apr_status_t stat;
//...
/*
*創(chuàng)建線程
*/
stat =apr_thread_create(&t1, NULL, thread_func1, NULL, pool);
stat =apr_thread_create(&t2, NULL, thread_func2, NULL, pool);
//...
apr_terminate();
return 0;
}
//output
... ...
[threadA]: own the lock, time[10:10:15]
[threadB]: modify the var, time[10:10:15]
[threadA]: x = 10
[threadA]: Warning: x變量值被破壞,現(xiàn)重新修正之
[threadA]: release the lock, time[10:10:15]
當(dāng)然這個(gè)例子不一定很精確的表明threadB在threadA擁有互斥量的時(shí)候修改了x值。
二、條件變量
互斥量一般用于被設(shè)計(jì)被短時(shí)間持有的鎖,一旦我們不能確定等待輸入的時(shí)間時(shí),我們可以使用條件變量來完成同步。我們?cè)?jīng)說過I/O復(fù)用,在我們調(diào)用poll或者select的時(shí)候?qū)嶋H上就是在內(nèi)核與用戶進(jìn)程之間達(dá)成了一個(gè)協(xié)議,即當(dāng)某個(gè)I/O描述符事件發(fā)生的時(shí)候內(nèi)核通知用戶進(jìn)程并且將處于掛起狀態(tài)的用戶進(jìn)程喚醒。而這里我們所說的條件變量讓對(duì)等的線程間達(dá)成協(xié)議,即“某一線程發(fā)現(xiàn)某一條件滿足時(shí)必須發(fā)信號(hào)給阻塞在該條件上的線程,將后者喚醒”。這樣我們就有了兩種角色的線程,分別為
(1)給條件變量發(fā)送信號(hào)的線程
其流程大致為:
{
獲取條件變量關(guān)聯(lián)鎖;
修改條件為真;
調(diào)用apr_thread_cond_signal通知阻塞線程條件滿足了;------(a)
釋放變量關(guān)聯(lián)鎖;
}
(2)在條件變量上等待的線程
其流程大致為:
{
獲取條件變量關(guān)聯(lián)鎖;
while (條件為假) {--------------------- (c)
調(diào)用apr_thread_cond_wait阻塞在條件變量上等待;------(b)
}
修改條件;
釋放變量關(guān)聯(lián)鎖;
}
上面兩個(gè)流程中,理解三點(diǎn)最關(guān)鍵:
a) apr_thread_cond_signal中調(diào)用的pthread_cond_signal保證至少有一個(gè)阻塞在條件變量上的線程恢復(fù);在《Unix網(wǎng)絡(luò)編程Vol2》中也談過這里存在著一個(gè)race。即在發(fā)送cond信號(hào)的同時(shí),該發(fā)送線程仍然持有條件變量關(guān)聯(lián)鎖,那么那個(gè)恢復(fù)線程的apr_thread_cond_wait返回時(shí)仍然拿不到這把鎖就會(huì)再次掛起。這里的這個(gè)race要看各個(gè)平臺(tái)實(shí)現(xiàn)是如何處理的了。
b) apr_thread_cond_wait中調(diào)用的pthread_cond_wait原子的將調(diào)用線程掛起,并釋放其持有的條件變量關(guān)聯(lián)鎖;
c)這里之所以使用while反復(fù)測(cè)試條件,是防止“偽喚醒”的存在,即條件并未滿足就被喚醒。所以無論怎樣,喚醒后我都需要重新測(cè)試一下條件,保證該條件的的確確滿足了。
條件變量在解決“生產(chǎn)者-消費(fèi)者”問題中有很好的應(yīng)用,在我以前的一篇blog中也說過這個(gè)問題。
三、讀寫鎖
前面說過,互斥量把想進(jìn)入臨界區(qū)而又試圖獲取互斥量的所有線程都阻塞住了。讀寫鎖則改進(jìn)了互斥量的這種霸道行為,它區(qū)分讀臨界區(qū)數(shù)據(jù)和修改臨界區(qū)數(shù)據(jù)兩種情況。這樣如果有線程持有讀鎖的話,這時(shí)再有線程想讀臨界區(qū)的數(shù)據(jù)也是可以再獲取讀鎖的。讀鎖和寫鎖的分配規(guī)則在《Unix網(wǎng)絡(luò)編程Vol2》中有詳細(xì)說明,這里不詳述。
四、小結(jié)
三種同步方式如何選擇?場(chǎng)合不同選擇也不同。互斥量在于完全同步的臨界區(qū)訪問;條件變量在解決“生產(chǎn)者-消費(fèi)者”模型問題上有獨(dú)到之處;讀寫鎖則在區(qū)分對(duì)臨界區(qū)讀寫的時(shí)候使用。