天天看點

apr多線程

 線上程同步方面,Posix标準定義了3種同步模型,分别為互斥量、條件變量和讀寫鎖。APR也“淺”封裝了這3種模型,隻是在“讀寫鎖”一塊兒還沒有全部完成。

線程同步的源代碼的位置在$(APR_HOME)/locks目錄下,本篇blog着重分析unix子目錄下的thread_mutex.c、thread_rwlock.c和thread_cond.c檔案的内容,其相應頭檔案為(APR_HOME)/include/apr_thread_mutex.h、apr_thread_rwlock.h和apr_thread_cond.h。

由于APR的封裝過于“淺顯”,實際上也并沒有多少值得分析的“靓點”。是以本篇實際上是在讨論線程同步的3種運作模型。

一、互斥量

互斥量是線程同步中最基本的同步方式。互斥量用于保護代碼中的臨界區,以保證在任一時刻隻有一個線程或程序通路臨界區。

1、互斥量的初始化

在POSIX Thread中提供兩種互斥量的初始化方式,如下:

(1) 靜态初始化

互斥量首先是一個變量,Pthread提供預定義的值來支援互斥量的靜态初始化。舉例如下:

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

既然是靜态初始化,那麼必然要求上面的mutex變量需要靜态配置設定。在APR中并不支援apr_thread_mutex_t的使用預定值的靜态初始化(但可以變通的利用下面的方式進行靜态配置設定的mutex的初始化)。

(2) 動态初始化

除了上面的情況,如果mutex變量在堆上或在共享記憶體中配置設定的話,我們就需要調用一個初始化函數來動态初始化該變量了。在Pthread中的對應接口為pthread_mutex_init。APR封裝了這一接口,我們可以使用下面方式在APR中初始化一個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、互斥鎖的軟弱性所在

互斥鎖之軟弱性在于其是一種協作性鎖,其運作時對各線程有一定的要求,即“所有要通路臨界區的線程必須首先擷取這個互斥鎖,離開臨界區後釋放該鎖”,一旦某一線程不遵循該要求,那麼這個互斥鎖就形同虛設了。如下面的例子:

舉例:我們有兩個線程,一個線程A遵循要求,每次通路臨界區均先擷取鎖,然後将臨界區的變量x按偶數值遞增,另一個線程B不遵循要求直接修改x值,這樣即使線上程A擷取鎖的情況下仍能修改臨界區的變量x。

static apr_thread_mutex_t       *mutex  = NULL;

static int                                x       = 0;

static apr_thread_t             *t1     = NULL;

static apr_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變量值被破壞,現重新修正之\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;

        //...

        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變量值被破壞,現重新修正之

[threadA]: release the lock, time[10:10:15]

當然這個例子不一定很精确的表明threadB在threadA擁有互斥量的時候修改了x值。

二、條件變量

互斥量一般用于被設計被短時間持有的鎖,一旦我們不能确定等待輸入的時間時,我們可以使用條件變量來完成同步。我們曾經說過I/O複用,在我們調用poll或者select的時候實際上就是在核心與使用者程序之間達成了一個協定,即當某個I/O描述符事件發生的時候核心通知使用者程序并且将處于挂起狀态的使用者程序喚醒。而這裡我們所說的條件變量讓對等的線程間達成協定,即“某一線程發現某一條件滿足時必須發信号給阻塞在該條件上的線程,将後者喚醒”。這樣我們就有了兩種角色的線程,分别為

(1) 給條件變量發送信号的線程

其流程大緻為:

{

        擷取條件變量關聯鎖;

        修改條件為真;

        調用apr_thread_cond_signal通知阻塞線程條件滿足了;------ (a)

        釋放變量關聯鎖;

}

(2) 在條件變量上等待的線程

其流程大緻為:

{

        擷取條件變量關聯鎖;

        while (條件為假) { --------------------- (c)

                調用apr_thread_cond_wait阻塞在條件變量上等待;------ (b)

        }

        修改條件;

        釋放變量關聯鎖;

}

上面兩個流程中,了解三點最關鍵:

a) apr_thread_cond_signal中調用的pthread_cond_signal保證至少有一個阻塞在條件變量上的線程恢複;在《Unix網絡程式設計 Vol2》中也談過這裡存在着一個race。即在發送cond信号的同時,該發送線程仍然持有條件變量關聯鎖,那麼那個恢複線程的apr_thread_cond_wait傳回時仍然拿不到這把鎖就會再次挂起。這裡的這個race要看各個平台實作是如何處理的了。

b) apr_thread_cond_wait中調用的pthread_cond_wait原子的将調用線程挂起,并釋放其持有的條件變量關聯鎖;

c) 這裡之是以使用while反複測試條件,是防止“僞喚醒”的存在,即條件并未滿足就被喚醒。是以無論怎樣,喚醒後我都需要重新測試一下條件,保證該條件的的确确滿足了。

條件變量在解決“生産者-消費者”問題中有很好的應用,在我以前的一篇blog中也說過這個問題。

三、讀寫鎖

前面說過,互斥量把想進入臨界區而又試圖擷取互斥量的所有線程都阻塞住了。讀寫鎖則改進了互斥量的這種霸道行為,它區分讀臨界區資料和修改臨界區資料兩種情況。這樣如果有線程持有讀鎖的話,這時再有線程想讀臨界區的資料也是可以再擷取讀鎖的。讀鎖和寫鎖的配置設定規則在《Unix網絡程式設計 Vol2》中有詳細說明,這裡不詳述。

四、小結

三種同步方式如何選擇?場合不同選擇也不同。互斥量在于完全同步的臨界區通路;條件變量在解決“生産者-消費者”模型問題上有獨到之處;讀寫鎖則在區分對臨界區讀寫的時候使用。

繼續閱讀