条件变量是什么
- 条件变量是线程可用的一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件产生。
- 条件变量本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
- 在使用条件变量之前必须进行初始化,可以把常量 PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对其进行初始化。
- 在释放条件变量底层的内存空间时,可以使用pthread_cond_destory函数对条件变量进行反初始化。
条件变量的函数
#include<pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
- 两个函数的返回值,成功都为0,;否则返回错误编号
- 除非要创建一个具有非默认属性的条件变量,否则pthread_cond_init函数的attr参数可以设置为NULL。
-
等待条件变量的函数
#include<pthread.h>
int pthread_cond_wait( pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
- 两个函数的返回值:若成功,返回0;若失败,返回错误编号
- 传递给pthread_cond_wait 的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥锁解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化,pthread_cond_wait返回时,互斥锁再次被锁住。
- pthread_cond_timedwait 函数的功能与pthread_cond_wait 函数相似,只是多了一个超时(tsptr)。超时值指定了我们愿意等待多长时间,他是通过timespec结构体定义的,这个结构体暂时不去深究,后面的博客会说到。
- 如果超时到期时条件还是没有出现,pthread_cond_timewait 将重新获取互斥量,然后返回错误ETIMEDOUT。从pthread_cond_wait或者pthread_cond_timewait 调用成功时,线程需要重新计算条件,因为一个线程可能已经在运行并改变了条件。
-
通知线程条件
#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 函数则能唤醒等待该条件的所有线程。
- 在调用pthread_cond_signal 或者pthread_cond_broadcast 时,我们说这是给线程或者条件发信号。必须注意:一定要在改变条件状态之后再给线程发信号。
生产者消费者模型
- 我们利用生产者消费者模型来对条件变量做进一步的认识。
- 我们就先来介绍一个生产者消费者模型
- 那我就用我自己的话来描述一下,这个模型有三个条件:生产者、消费者、仓库(一段内存)。假设现在有一个生产者和一个消费者。那我们这样类比,生产者是卖螺蛳粉的老板,消费者是吃货。比如吃货想吃螺蛳粉,现在老板有三种状态,我有螺蛳粉,直接放入仓库中,吃货去吃就好;我没有螺蛳粉,但是我现在生产螺蛳粉;第三种情况,我还没有上班,相当于老板这个线程没有拿到cpu时间片。那么后两种情况的话,吃货只能等待,等待老板生产螺蛳粉。接下来如果老板生产了很多螺蛳粉,并且仓库里快放满了,这时就需要通知一下吃货过来吃螺蛳粉;亦或者我生产一碗螺蛳粉就通知吃货过来吃。
- 如果对应到多个生产者和多个消费者,套路也是一样的,好,我们来看一下用条件变量加上互斥锁实现的生产者消费者模型。
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 #include<time.h>
6
7 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
8 pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
9
10
11 typedef struct Food{
12 int food;
13 struct Food* next;
14 }Food;
15
16 Food* head = NULL;
17
18 void *product(void* arg)
19 {
20 Food* mp;
21 while(1)
22 {
23 mp = (Food*)malloc(sizeof(Food));
24 mp->food = rand()%1000+1;
25 printf("product : %d\n",mp->food);
26
27 pthread_mutex_lock(&mutex);
28 mp->next = head;
29 head = mp;
30 pthread_mutex_unlock(&mutex);
31
32 pthread_cond_signal(&has_product);
33 sleep(rand()%5);
34 }
35 return NULL;
36 }
37
38 void* consume(void* arg)
39 {
40 Food* mp;
41 while(1)
42 {
43 pthread_mutex_lock(&mutex);
44 while(head == NULL)
45 {
46 pthread_cond_wait(&has_product,&mutex);
47 }
48
49 mp = head;
50 head = mp->next;
51 pthread_mutex_unlock(&mutex);
52 printf("consume : %d\n",mp->food);
53 free(mp);
54 sleep(rand()%5);
55 }
56 return NULL;
57 }
58
59 int main()
60 {
61 pthread_t tid1,tid2;
62 int ret;
63 srand(time(NULL));
64
65 ret = pthread_create(&tid1,NULL,product,NULL);
66 if(ret != 0)
67 {
68 fprintf(stderr,"product pthread create error:%lu\n",strerror(ret));
69 exit(0);
70 }
71 ret = pthread_create(&tid2,NULL,consume,NULL);
72 if(ret != 0)
73 {
74 fprintf(stderr,"consume pthread create error:%lu\n",strerror(ret));
75 exit(1);
76 }
77
78 pthread_join(tid1,NULL);
79 pthread_join(tid2,NULL);
80
81 return 0;
82 }
- 随便截取一段程序运行结果
- 条件是工作链表的状态,我们互斥量来保护条件。在while循环中判断条件。把消息放到工作链表中,需要占有互斥量,但在给等待线程发信号时,不需要占有互斥量。只要线程在调用pthread_cond_signal 之前把消息从链表中销毁了,就可以在释放互斥量以后完成这部分工作。因为我们是在while循环中检查条件,所以不存在这样的问题:线程醒来,发现链表头结点为空,然后返回继续等待。如果代码不能容忍这种竞争,就需要在给线程发信号的时候占有互斥量。
条件变量的优点
- 相较于mutex而言,条件变量可以减少竞争。如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
信号量
- 进化版的互斥锁(1 --> N),由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
- 信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
主要应用函数:
sem_init函数
sem_destroy函数
sem_wait函数
sem_trywait函数
sem_timedwait函数
sem_post函数
- 以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)
- sem_t 类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
- sem_t sem; 规定信号量sem不能 < 0。头文件 <semaphore.h>
信号量基本操作:
- sem_wait : 信号量大于0,则信号量 - - ,信号量等于0,就会造成线程阻塞。
( 类比 pthread_mutex_lock,我们可以认为初始化后的互斥锁值为1,加锁就减减,变为0。如果为锁值为0,那么线程阻塞。) - sem_post:将信号量++,同时唤醒阻塞在信号量上的线程(类比pthread_mutex_unlock,我们可以认为解锁加加,从0变为1,如果锁的值为1,那么说明这个锁是解开的)
- 但是由于sem_t的实现对用户隐藏,所以所谓的+ +、- - 操作只能通过函数来实现,而不能直接++、–符号。我们这里只是类比一下,更好理解而已。
- 信号量的初值,决定了占用信号量的线程的个数。
- sem_init函数,初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参1:sem信号量
参2:pshared取0用于线程间;取非0(一般为1)用于进程间
参3:value指定信号量初值
- sem_destroy函数,销毁一个信号量
int sem_destroy(sem_t *sem);
- sem_wait函数,给信号量加锁
int sem_wait(sem_t *sem);
- sem_post函数,给信号量解锁
int sem_post(sem_t *sem);
- sem_trywait函数,尝试对信号量加锁
(与sem_wait的区别类比lock和trylock)
int sem_trywait(sem_t *sem);
- sem_timedwait函数,限时尝试对信号量加锁
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参2:abs_timeout采用的是绝对时间。
定时1秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参
利用信号量来实现生产者消费者模型
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<pthread.h>
4 #include<semaphore.h>
5 #include<time.h>
6
7 #define NUM 5
8 int queue[NUM];
9 sem_t sem_black;
10 sem_t sem_product;
11
12
13 void* product(void* arg)
14 {
15 int i = 0;
16 while(1)
17 {
18 sem_wait(&sem_black);
19 queue[i] = rand()%1000+1;
20 printf("-----product : %d\n",queue[i]);
21 sem_post(&sem_product);
22
23 i = (i+1)%NUM;
24
25 sleep(rand()%3);
26 }
27 return NULL;
28 }
29
30 void* consume(void* arg)
31 {
32 int i = 0;
33 while(1)
34 {
35 sem_wait(&sem_product);
36 printf("-consum :%d\n",queue[i]);
37 queue[i] = 0;
38 sem_post(&sem_black);
39
40 i = (i+1)%NUM;
41 sleep(rand()%3);
42 }
43
44 return NULL;
45 }
46
47 int main()
48 {
49 pthread_t tid1,tid2;
50 srand(time(NULL));
51
52 sem_init(&sem_black,0,NUM);
53 sem_init(&sem_product,0,0);
54
55 pthread_create(&tid1,NULL,product,NULL);
56 pthread_create(&tid2,NULL,consume,NULL);
57
58 pthread_join(tid1,NULL);
59 pthread_join(tid2,NULL);
60
61 sem_destroy(&sem_product);
62 sem_destroy(&sem_black);
63
64 return 0;
65 }
- 随便截取一段程序运行结果,我们可以发现信号量更好的体现了线程的并发性。