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

原文地址:https://www.cnblogs.com/wangzhebufangqi/p/14227186.html