c語言並行程式設計之路(四)(barrier的實現和條件變數)
0.前言
接下來看共享記憶體程式設計的另一個問題:通過保證所有執行緒在程式中處於同一個位置來同步執行緒。這個同步點稱為barrier,翻譯為路障、柵欄等。只有所有執行緒都抵達此路障,執行緒才能繼續執行下去,否則會阻塞在路障處。
1.實現
1.1忙等待和互斥量
用忙等待和互斥量來實現路障比較直觀:使用一個由互斥量保護的共享計數器。當計數器的值表明每個執行緒都已經進入臨界區,所有執行緒就可以離開忙等待的狀態了。
/* Shared and initialized by the main thread*/ int counter; /*Initialize to 0*/ int thread_count; pthread_mutex_t barrier_mutex; ... void* Thread_work(...){ ... /*Barrier*/ pthread_mutex_lock(&barrier_mutex); counter++; pthread_mutex_unlock(&barrier_mutex); while(counter<thread_count); ... }
缺點:
- 執行緒處於忙等待迴圈時浪費了很多CPU週期,並且當程式中的執行緒數多過於核數時,程式的效能會直線下降。
- 若想使用這種實現方式的路障,則有多少個路障就必須要有多少個不同的共享counter變數來進行計數。
1.2訊號量
可以用訊號量來實現路障,能解決採用忙等待和互斥量實現路障的方式裡出現的問題。
/*Shared variables*/ int counter; /*Initialized to 0*/ sem_t count_sem; /*Initialized to 1*/ sem_t barrier_sem; /*Initialized to 0*/ ... void* Thread_work(...){ ... /*Barrier*/ sem_wait(&count_sem); if(counter == thread_count-1){ counter = 0; sem_post(&count_sem); for(j = 0; j<thread_count-1; ++j) sem_post(&barrier_sem); }else{ counter++; sem_post(&count_sem); sem_wait(&barrier_sem); } }
在忙等待實現的路障中,使用了一個計數器counter來判斷有多少執行緒抵達了路障。在這裡,採用了兩個訊號量:count_sem,用於保護計數器;barrier_sem,用於阻塞已經進入路障的執行緒。
執行緒被阻塞在sem_wait不會消耗CPU週期,所以用訊號量實現路障的方法比用忙等待實現的路障效能更佳。
如果想執行第二個路障,counter和count_sem可以重用,但是重用barrier_sem會導致競爭條件。
1.3條件變數
在pthreads中實現路障的更好方法是採用條件變數
,條件變數是一個數據物件,允許執行緒在某個特定條件或事件發生前都處於掛起狀態。當條件或事件發生時,另一個執行緒可以通過訊號來喚醒掛起的執行緒。一個條件變數總是與一個互斥量相關聯。
條件變數的一般使用方法與下面的虛擬碼類似:
lock mutex;
if condition has occurred
signal thread(s);
else{
unlock the mutex and block;
/*when thread is unblocked. mutex is relocked*/
}
unlock mutex;
Pthreads執行緒庫中的條件變數型別為pthread_cond_t。函式
int pthread_cond_signal(pthread_cond_t* cond_var_p /*in/out*/);
的作用是解鎖一個阻塞的執行緒,而函式
int pthread_cond_broadcast(pthread_cond_t* cond_var_p /*in/out*/);
的作用是解鎖所有被阻塞的執行緒。函式
int pthread_cond_wait(
pthread_cond_t* cond_var_p /*in/out*/,
pthread_mutex_t* mutex_p /*in/out*/);
的作用是通過互斥量mutex_p來阻塞執行緒,知道其他執行緒呼叫pthread_cond_signal或者pthread_cond_broadcast來解鎖它。當執行緒解鎖後,它重新獲得互斥量,所以實際上,pthread_cond_wait相當於按順序執行了以下的函式:
pthread_mutex_unlock(&mutex_p);
wait_on_signal(&cond_var_p);
pthread_mutex_lock(&mutex_p);
下面的程式碼使用條件變數實現路障:
/*Shared*/
int counter=0;
pthread_mutex_t mutex;
pthread_cond_t cond_var;
...
void* Thread_work(...){
...
/*Barrier*/
pthread_mutex_lock(&mutex);
counter++;
if(counter == thread_count){
counter == 0;
pthread_cond_broadcast(&cond_var);
}else{
while(pthread_cond_wait(&cond_var, &mutex) != 0);
}
pthread_mutex_unlock(&mutex);
...
}
之所以將pthread_cond_wait語句放置在while語句內,是為了確保被掛起的執行緒是被broadcast函式或signal函式喚醒的,檢查其返回值是否為0,若不為0,則被其他事件解除阻塞的執行緒會再次執行該函式再次掛起。
與互斥量和訊號量一樣,條件變數也應該初始化和銷燬。對應的函式是:
int pthread_cond_init(
pthread_cond_t* cond_p /*out*/,
const pthread_condattr_t* cond_attr_p /*in*/);
int pthread_cond_destroy(pthread_cond_t* cond_p /* in/out*/ );
2.參考資料
《並行程式設計導論》 4.8