线程同步—条件变量
条件变量
互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待这一通知,在通知未到达之前,线程处于阻塞状态。条件变量本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
条件变量总是结合互斥量使用的。条件变量就共享变量/临界资源的状态改变发出通知,而互斥量提供对该条件变量的互斥访问。
在使用条件变量之前,必须先对它进行初始化。由 pthread_cond_t 数据类型表示的条件变量可以用两种方式进行初始化,可以把常量 PTHREAD_COND_INITIALIZER 赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用pthread_cond_init() 函数对它进行初始化。
在释放条件变量底层的内存空间之前,可以使用 pthread_cond_destroy() 函数对条件变量进行反初始化(deinitialize)。
1 include <pthread.h>
2
3 int pthread_cond_init(pthread_cond_t *restrict cond,
4 const pthread_condattr_t *restrict attr);
5
6 int pthread_cond_destroy(pthread_cond_t *cond);
7
8 //条件变量的静态初始化方式
9 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 两个函数的返回值:若成功,返回0;否则,返回错误编号。
除非需要创建一个具有非默认属性的条件变量,否则 pthread_cond_init() 函数的 attr 参数可以设置为 NULL。
条件变量的操作
条件变量的操作主要是发送通知信号(Signal)和等待(Wait)。发送信号操作即通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变。等待操作是指在收到一个通知前一直处于阻塞状态。
1. 设置等待条件
我们使用 pthread_cond_wait() 函数等待条件变量为真。如果在给定的时间内条件不能满足,那么会返回一个错误编号。
1 #include <pthread.h>
2
3 int pthread_cond_wait(pthread_cond_t *restrict cond,
4 pthread_mutex_t *restrict mutex);
5
6 int pthread_cond_timedwait(pthread_cond_t *restrict cond,
7 pthread_mutex_t *restrict mutex,
8 const struct timespec *restrict abstime);
- 两个函数的返回值:若成功,返回0;否则,返回错误编号。
《函数说明》
(1)传递给 pthread_cond_wait() 函数的互斥量对条件变量进行保护。调用者把锁住的互斥量传给该函数,函数自动把调用线程放到等待条件的线程列表中,然后在 pthread_cond_wait() 函数内部对互斥量解锁。在未接收到条件变量状态改变的通知之前,当前线程会阻塞在 pthread_cond_wait() 函数中;一旦接收到状态改变的通知“信号”,pthread_cond_wait() 才会返回,并且在该函数内部互斥量会被再次锁定,因此,该函数返回后,还需要对互斥量进行一次解锁操作。
(2)pthread_cond_timedwait() 函数的功能与 pthread_cond_wait() 函数类似,只是多了一个超时时间。超时值指定了我们愿意等待多长时间,它是通过 timespec 结构体指定的。这个时间值是一个绝对数而不是一个相对数。例如,假设愿意等待3分钟,那么,并不是把3分钟转换成 timespec 结构,而是需要把当前时间加上3分钟再转换成 timespec 结构。可以使用 clock_gettime() 函数获取 timespec 结构体表示的当前时间,但是并不是所有的平台都支持这个函数,Linux系统是支持的,clock_gettime() 函数是在librt库中实现的,所以需要加上-lrt库链接。当然,也可以使用另一个函数 gettimeofday() 获取 timeval 结构表示的当前时间,然后把这个时间转换成 timespec 结构体。要得到超时值的绝对时间,可以使用下面的函数(假设阻塞的最大时间使用分钟来表示):
#include <stdlib.h>
#include <sys/time.h>
void maketimeout(struct timespec *tsp, long minutes)
{
struct timeval now;
//get the current time
gettimeofday(&now, NULL);
tsp->tv_sec = now.tv_sec;
tsp->tv_nsec = now.tv_usec * 1000; //usec(微秒)-->nsec(纳秒)
//add the offset to get timeout value
tsp->tv_sec += minutes * 60;
}
<链接> struct timespec 和 struct timeval 结构体定义
如果超时时间到期后,条件还是没有出现,pthread_cond_timedwait() 将重新获取互斥量,然后返回错误 ETIMEDOUT。从 pthread_cond_wait 或者 pthread_cond_timedwait 调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。
2. 设置通知条件
有两个函数可以用于通知线程条件已经满足。pthread_cond_signal() 函数至少能唤醒一个等待该条件的线程,而 pthread_cond_broadcast() 函数则能唤醒等待该条件的所有线程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- 两个函数的返回值:若成功,返回0;否则,返回错误编号。
在调用 pthread_cond_signal 或 pthread_cond_broadcast 时,我们说这是给线程或者条件发送通知信号(Signal)。必须注意的是,一定要在改变状态以后再给线程发通知信息。
<备注> POXIS 规范为了简化 pthread_cond_signal 的实现,允许它在实现的时候唤醒一个以上的线程。
示例:在生产者-消费者模型中,结合使用条件变量和互斥量对线程进行同步。代码如下:prod_condvar.c
#include <stdio.h> #include <stdbool.h> #include <unistd.h> #include <time.h> #include <pthread.h> //对静态互斥量的初始化 static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //对静态条件变量的初始化 static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; static int avail = 0; //共享变量,记录已生产可供消息的产品数量 static void * producer(void *arg) { int cnt = atoi((char*)arg); int ret, i; printf("producer: pid=%lu, tid=%lu\n", getpid(), pthread_self()); for(i=0; i<cnt; i++){ sleep(1); ret = pthread_mutex_lock(&mtx); //对共享变量 avail 需要互斥访问 if(ret != 0) printf("pthread_mutex_lock failed!\n"); avail++; //生成一个产品 ret = pthread_mutex_unlock(&mtx); if(ret != 0) printf("pthread_mutex_unlock failed!\n"); ret = pthread_cond_signal(&cond); //唤醒消费者 if(ret != 0) printf("pthread_cond_signal failed!\n"); } return NULL; } int main(int argc, char *argv[]) { pthread_t tid; int ret, i; int totRequired; //所有线程将要生产的产品的总数 int numConsumed; //消费者已消费的产品数 bool done; //商品是否消费完成标志 time_t t; t = time(NULL); printf("main: pid=%lu, tid=%lu\n", getpid(), pthread_self()); //创建所有线程 totRequired = 0; for(i=1; i<argc; i++){ totRequired += atoi(argv[i]); ret = pthread_create(&tid, NULL, producer, argv[i]); if(ret != 0){ printf("pthread_create failed!\n"); } } //消费者循环消费已生产出来的产品 numConsumed = 0; done = false; for(;;){ pthread_mutex_lock(&mtx); if(avail == 0){ ret = pthread_cond_wait(&cond, &mtx); //等待唤醒通知 if(ret != 0) printf("pthread_cond_wait failed!\n"); } //程序运行到这里时,互斥量仍是lock的 while(avail > 0){ numConsumed ++; //消费者已消费商品数加1 avail --; //现存商品数减1 printf("T=%ld, numConsumed=%d\n", (long)(time(NULL)-t), numConsumed); done = numConsumed >= totRequired; //当所有生产的商品都已消费完成,done置为true } pthread_mutex_unlock(&mtx); if(done) break; } return 0; }
**编译命令: gcc prod_condvar.c -o prod_condvar -lpthread
**运行命令: ./prod_condvar 4 5 6
**运行结果:
main: pid=20056, tid=140332428621632 producer: pid=20056, tid=140332420314880 producer: pid=20056, tid=140332411922176 producer: pid=20056, tid=140332403529472 T=1, numConsumed=1 T=1, numConsumed=2 T=1, numConsumed=3 T=2, numConsumed=4 T=2, numConsumed=5 T=2, numConsumed=6 T=3, numConsumed=7 T=3, numConsumed=8 T=3, numConsumed=9 T=4, numConsumed=10 T=4, numConsumed=11 T=4, numConsumed=12 T=5, numConsumed=13 T=5, numConsumed=14 T=6, numConsumed=15
《代码分析》
- 运行命令:./prod_condvar 4 5 6,表示的含义是生产者线程1、2、3生产的产品个数分别是4、5、6,共计15个。从运行结果可以看到,本示例中,共有4个线程,其中主线程是main函数,亦即消费者线程,而其他三个线程是生产者线程producer,也就是说总共有3个生产者,1个消费者。这四个线程同时共享全局变量 avail。
- 生产者负责生产商品,当生产者每生产出1个商品,共享变量avail自增加1,然后使用 pthread_cond_signal 唤醒main函数中的消费者线程,通知其可以消费商品了。而对于消费者线程,刚开始的时候,avail == 0,因此使用 pthread_cond_wait 设置等待条件,此时消费者线程会处于阻塞状态,直到接收到生产者 producer 发出的唤醒通知,消费者线程开始继续执行,并开始消费已生产出来的商品。
- 当消费者已消费的商品数 >= 所有生产者生产出来的商品时,退出 for循环,结束主线程,同时整个进程结束。
参考
《UNIX环境高级编程(第3版)》第11.6.6章节
《Linux_Unix系统编程手册(上)》第30.2章节