1. 程式人生 > 實用技巧 >c語言並行程式設計之路(四)(barrier的實現和條件變數)

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