条件变量
使用条件变量控制线程同步时,线程访问共享资源的前提,是程序中设置的条件变量得到满足。条件变量不会对共享资源加锁,但也会使线程阻塞,若线程不满足条件变量规定的条件,就会进入阻塞状态直到条件满足。
条件变量往往与互斥锁搭配使用,在线程需要访问共享资源时,会先绑定一个互斥锁,然后检测条件变量,若条件变量满足,线程就继续执行,并在资源访问完成后解开互斥锁;若条件变量不满足,线程将解开互斥锁,进入阻塞状态,等待条件变量状况发生改变。一般条件变量的状态由其它非阻塞态的线程改变,条件变量被满足则会唤醒阻塞中的进程,这些线程再次争夺互斥锁,对条件变量状况进行测试。
综上所述,条件变量的使用分为以下四个步骤:
(1)初始化条件变量;
(2)等待条件变量满足;
(3)唤醒阻塞线程;
(4)释放条件变量。
针对以上步骤,Linux系统中提供了一组与条件变量相关的系统调用,此组系统调用都存在于函数库pthread.h中,下面将对这些系统调用进行讲解。
① 初始化条件变量
Linux系统中用于初始化条件变量的函数为pthread_cond_init(),其声明如下:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
函数pthread_cond_init()中的参数cond代表条件变量,本质是一个指向pthread_cond_t类型的结构体指针,pthread_cond_t是Linux系统中定义的条件变量类型。参数attr代表条件变量的属性,通常设置为NULL,表示使用默认属性初始化条件变量,其默认值为PTHREAD_PROCESS_PRIVATE,表示当前进程中的线程共用此条件变量;也可将attr设置为PTHREAD_PROCESS_SHARED,表示多个进程间的线程共用条件变量。
若函数pthread调用成功则返回0,否则返回-1,并设置errno。
除使用函数pthread_cond_init()动态初始化条件变量外,也可以使用如下语句以静态方法初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
此种方式与将attr参数初始化为NULL的pthread_cond_init()函数等效,但是不进行错误检查。
② 阻塞等待条件变量
Linux系统中一般通过pthread_cond_wait()函数,使线程进入阻塞状态,等待一个条件变量,其声明如下:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
函数pthread_cond_wait()中的参数cond代表条件变量;参数mutex代表与当前线程绑定的互斥锁。若该函数调用成功则返回0,否则返回-1,并设置errno。
pthread_cond_wait()类似于互斥锁中的函数pthread_mutex_lock(),但其功能更为丰富,它的工作机制如下:
(1)阻塞等待条件变量cond满足;
(2)解除已绑定的互斥锁(类似于pthread_mutex_unlock());
(3)当线程被唤醒,pthread_cond_wait()函数返回,pthread_cond_wait()函数同时会解除线程阻塞,并使线程重新申请绑定互斥锁。
以上工作机制中,前两条为一个原子操作;需要注意的最后一条,最后一条机制表明:当线程被唤醒后,仍需重新绑定互斥锁。这是因为,“线程被唤醒”及“绑定互斥锁”并不是一个原子操作,条件变量满足后也许会有多个处于运行态的线程出现,并竞争互斥锁,极有可能在线程B绑定互斥锁之前,线程A已经执行了以下操作:获取互斥锁——修改条件变量——解除互斥锁,此时线程B即便获取到互斥锁,条件变量仍不满足,线程B应继续阻塞等待。综上所述,再次检测条件变量的状况是极有必要的。
如图1展示了条件变量机制控制程序逻辑流程的示意图。
图1 条件变量控制流程示意图
除pthread_cond_wait()外,pthread_cond_timedwait()也能使线程阻塞等待条件变量,不同的是,该函数可以指定线程的阻塞时长,若等待超时,该函数便会返回。函数pthread_cond_timedwait()存在于函数库pthread.h中,其声明如下:
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
函数pthread_cond_timedwait()中的参数cond代表条件变量,参数mutex代表互斥锁,参数abstime代表绝对时间,用于设置等待时长,该参数是一个传入参数,本质是一个struct timespec类型的结构体指针,该结构体的定义如下:
struct timespec {
time_t tv_sec; //秒
long tv_nsec; //纳秒
}
① pthread_cond_signal()
pthread_cond_signal()函数会在条件变量满足之后,以信号的形式唤醒阻塞在该条件变量的一个线程。处于阻塞状态中的线程的唤醒顺序由调度策略决定。pthread_cond_signal()函数存在于函数库pthread.h中,其声明如下:
int pthread_cond_signal(pthread_cond_t *cond);
函数pthread_cond_signal()中的参数cond代表条件变量,若该函数调用成功则返回0,否则返回-1,并设置errno。
② pthread_cond_broadcast()
pthread_cond_broadcast()函数同样唤醒阻塞在指定条件变量的线程,不同的是,该函数会以广播的形式,唤醒阻塞在该条件变量上的所有线程。pthread_cond_broadcast()函数存在于函数库pthread.h中,其声明如下:
int pthread_cond_broadcast(pthread_cond_t *cond);
函数pthread_cond_broadcast()中的参数cond代表条件变量,若该函数调用成功则返回0,否则返回-1,并设置errno。
下面通过一个案例来展示使用条件变量实现线程同步的方法。
案例10:生产者-消费者模型是线程同步中的一个经典案例。假设有两个线程,这两个线程同时操作一个共享资源(一般称为汇聚),其中一个模拟生产者行为,生产共享资源,当容器存满时,生产者无法向其中放入产品;另一个线程模拟消费者行为,消费共享资源,当产品数量为0时,消费者无法获取产品,应阻塞等待。显然,为防止数据混乱,每次只能由生产者、消费者中的一个,操作共享资源。本案例要求使用程序实现简单的生产者-消费者模型(可假设容器无限大)。
案例实现如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <pthread.h>
5 struct msg {
6 struct msg *next;
7 int num;
8 };
9 struct msg *head;
10 pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; //初始化条件变量
11 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //初始化互斥锁
12 //消费者
13 void *consumer(void *p)
14 {
15 struct msg *mp;
16 for (;;) {
17 pthread_mutex_lock(&lock); //加锁
18 //若头结点为空,表明产品数量为0,消费者无法消费产品
19 while (head == NULL) {
20 pthread_cond_wait(&has_product, &lock); //阻塞等待并解锁
21 }
22 mp = head;
23 head = mp->next; //模拟消费一个产品
24 pthread_mutex_unlock(&lock);
25 printf("-Consume ---%d\n", mp->num);
26 free(mp);
27 sleep(rand() % 5);
28 }
29 }
30 //生产者
31 void *producer(void *p)
32 {
33 struct msg *mp;
34 while (1) {
35 mp = malloc(sizeof(struct msg));
36 mp->num = rand() % 1000 + 1; //模拟生产一个产品
37 printf("-Produce ---%d\n", mp->num);
38 pthread_mutex_lock(&lock); //加锁
39 mp->next = head; //插入结点(添加产品)
40 head = mp;
41 pthread_mutex_unlock(&lock); //解锁
42 pthread_cond_signal(&has_product); //唤醒等待在该条件变量上的一个线程
43 sleep(rand() % 5);
44 }
45 }
46 int main(int argc, char *argv[])
47 {
48 pthread_t pid, cid;
49 srand(time(NULL));
50 //创建生产者、消费者线程
51 pthread_create(&pid, NULL, producer, NULL);
52 pthread_create(&cid, NULL, consumer, NULL);
53 //回收线程
54 pthread_join(pid, NULL);
55 pthread_join(cid, NULL);
56 return 0;
57 }
案例10中第5~8行代码定义了一个链表结点,用于存储生产者线程创建的资源;第9行代码定义了链表的头结点,该链表是一个全局变量,因此是所有线程都可访问的公有资源;第10、11两行分别定义并初始化了互斥锁与条件变量;第13~29行代码中的函数consumer()用于模拟消费者的行为;第31~45行代码中的函数producer()用于模拟生产者的行为;第46~57行代码为主程序,主要用于创建生产者、消费者线程,和执行线程的回收工作。
由于本案例中未对容器容量进行限制,因此生产者只要能获取互斥锁,便能成功生产产品;但对消费者来说,总需先有产品才能消费,因此,无论案例执行多少次,第一行打印的总是生产者信息。编译案例10,执行程序,执行结果如下:
-Produce ---950
-Consume ---950
-Produce ---741
-Produce ---16
-Produce ---136
-Produce ---196
-Consume ---196
-Produce ---697
-Consume ---697
-Consume ---136
……
观察程序执行结果,由执行结果可知案例10实现成功。
在生产者-消费者模型中,生产者、消费者线程除受互斥锁限制,不能同时操作共享资源外,还受到条件变量的限制:对生产者线程而言,若共享资源区已满,生产者便无法向其中放入数据;对消费者线程而言,若共享资源区为空,消费者便无法从其中获取数据。
若在实现生产者-消费者模型时,创建了多个消费者线程,且程序中只使用互斥锁限制线程,那么不但生产者与消费者之间会竞争互斥锁,不同的消费者同样会竞争互斥锁;而添加条件变量后,只有在满足读取条件时,消费者之间才会产生竞争关系。由此可知,相比互斥锁,条件变量有效减少了线程间的竞争。读者可自行修改案例10代码,观察程序执行结果,对此进行验证。