文章目录:
1. 线程同步
作用:在多个执行流访问临界资源的时候是合理访问的
模拟一个如下场景,假设,有一个线程A,一个线程B,一个碗,线程A负责吃碗里的面,线程B负责给碗里做面,此时我们用如下代码进行模拟:
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4
5 int g_bowl = 1;
6
7 pthread_mutex_t My_lock;
8
9 //Eat
10 void* MythreadA(void* arg)
11 {
12 while(1)
13 {
14 pthread_mutex_lock(&My_lock);
15
16 printf("i eat %d,i am %p\n",g_bowl,pthread_self());
17
18 g_bowl--;
19
20 pthread_mutex_unlock(&My_lock);
21
22 }
23 return NULL;
24 }
25
26 //Make
27 void* MythreadB(void* arg)
28 {
29 while(1)
30 {
31 pthread_mutex_lock(&My_lock);
32
33 g_bowl++;
34
35 printf("i make %d,i am %p\n",g_bowl,pthread_self());
36
37
38 pthread_mutex_unlock(&My_lock);
39
40 }
41 return NULL;
42 }
43
44 int main()
45 {
46 //锁初始化
47 pthread_mutex_init(&My_lock,NULL);
48
49 pthread_t tid_Eat,tid_Make;
50
51 //创建两个线程
52 int ret = pthread_create(&tid_Eat,NULL,MythreadA,NULL);
53 if(ret < 0)
54 {
55 perror("pthread_create");
56 return 0;
57 }
58 pthread_create(&tid_Make,NULL,MythreadB,NULL);
59 if(ret < 0)
60 {
61 perror("pthread_create");
62 return 0;
63 }
64
65
66 //线程等待
67 pthread_join(tid_Eat,NULL);
68 pthread_join(tid_Make,NULL);
69
70 pthread_mutex_destroy(&My_lock);
71 return 0;
72 }
此时,我们将程序跑起来,可能会看到bowl已经减为负数,也有可能看到bowl加为超过1的正数,为什么会出现上述情况呢?因为线程A负责吃面,线程B负责做面,当线程B拿到CPU资源时,它可能持续往碗里做面,就会出现bowl>1的情况,当线程B拿到CPU资源时,它可能持续吃面,就会出现bowl<0的情况,如下图所示:
而线程同步就是解决如上问题的,同步,限制线程A什么时候吃面,线程B什么时候做面
2. 条件变量
条件变量 = PCB等待队列 + 一堆接口
2.1 PCB等待队列
PCB等待队列:当线程发现资源不可用的时候,调用条件变量接口,将自己放到PCB等待队列中,等待被唤醒
图解如下:
2.2 条件变量的接口
条件变量的类型:pthread_cond_t
2.2.1 初始化
静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- cond:待要初始化的“条件变量”的变量,一般情况下,传递一个pthread_cond_t类型变量的地址
- attr:一般情况下给NULL,采用默认属性
2.2.2 等待接口
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
- cond:待要初始化的“条件变量”的变量,一般情况下,传递一个pthread_cond_t类型变量的地址
- mutex:互斥锁
- 作用:如果一个执行流调用了该接口,就会将执行流对应的PCB放到参数cond的PCB等待队列当中
2.2.3 唤醒接口
int pthread_cond_signal(pthread_cond_t *cond);
- 作用:通知(唤醒)PCB等待队列当中的线程,如果被通知(唤醒)的线程接收到了,则从PCB等待队列当中出队操作,正常执行代码
- 注意:此接口至少唤醒一个PCB等待队列当中的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
- 注意:此接口唤醒所有PCB等待队列当中的线程
2.2.4 销毁接口
int pthread_cond_destroy(pthread_cond_t *cond);
2.3 代码验证
给上文模拟的吃面做面代码加上条件变量约束,线程A先判断碗里是否有面,若有面就吃面,吃完面,通知线程B做面,若碗里没有面,则线程A进行等待。线程B先判断碗里是否有面,若有面,则线程B进行等待,若没有面,则线程B做面,做完面后通知线程A吃面,改进后的代码如下:
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4
5 #define THREAD_NUM 1
6 int g_bowl = 1;
7
8 pthread_mutex_t My_lock;
9 pthread_cond_t g_cond;
10
11 //Eat
12 void* MythreadA(void* arg)
13 {
14 while(1)
15 {
16 pthread_mutex_lock(&My_lock);
17
18 //判断是否能吃面
19 if(g_bowl < 1)
20 {
21 //等待
22 pthread_cond_wait(&g_cond,&My_lock);
23 }
24 printf("i eat %d,i am %p\n",g_bowl,pthread_self());
25
26 g_bowl--;
27
28 pthread_mutex_unlock(&My_lock);
29 //通知做面
30 pthread_cond_signal(&g_cond);
31
32 }
33 return NULL;
34 }
35
36 //Make
37 void* MythreadB(void* arg)
38 {
39 while(1)
40 {
41 pthread_mutex_lock(&My_lock);
42
43 //判断是否需要做面
44 if(g_bowl > 0)
45 {
46 //等待
47 pthread_cond_wait(&g_cond,&My_lock);
48 }
49
50 g_bowl++;
51 printf("i make %d,i am %p\n",g_bowl,pthread_self());
52
53
54 pthread_mutex_unlock(&My_lock);
55 //通知吃面
56 pthread_cond_signal(&g_cond);
57
58 }
59 return NULL;
60 }
61
62 int main()
63 {
64 //锁初始化
65 pthread_mutex_init(&My_lock,NULL);
66 //初始化条件变量
67 pthread_cond_init(&g_cond,NULL);
68
69 pthread_t tid_Eat[THREAD_NUM],tid_Make[THREAD_NUM];
70
71 //创建线程
72 for(int i = 0;i < THREAD_NUM;i++)
73 {
74 int ret = pthread_create(&tid_Eat[i],NULL,MythreadA,NULL);
75 if(ret < 0)
76 {
77 perror("pthread_create");
78 return 0;
79 }
80 pthread_create(&tid_Make[i],NULL,MythreadB,NULL);
81 if(ret < 0)
82 {
83 perror("pthread_create");
84 return 0;
85 }
86
87 }
88
89 //线程等待
90 for(int i = 0;i < THREAD_NUM;i++)
91 {
92 pthread_join(tid_Eat[i],NULL);
93 pthread_join(tid_Make[i],NULL);
94 }
95
96 pthread_mutex_destroy(&My_lock);
97 pthread_cond_destroy(&g_cond);
98 return 0;
99 }
让程序跑起来,我们可以看到线程A吃一碗面,线程B做一碗面,如下图所示:
如上程序虽然看着符合我们的预期,实则,当我们将吃面和做面的线程分别创建两个,就会看到程序依然有错,如下图所示:
对如上运行错误进行分析,如下:
假设,碗里有面,此时make1拿到了锁,则make1判断碗里有面后将自己放入PCB等待队列中进行等待,然后释放互斥锁,假设此时,eat1拿到了互斥锁,然后eat1吃掉碗里的面,然后释放锁并通知PCB等待队列,此时make1已经出队,假设此时make2拿到了锁,并做了一碗面,然后释放锁,然后make1又拿到了锁,而此时make1将要执行的是pthread_cond_wait函数之后那条命令,则make1跳过了判断碗里是否有面,直接往碗里做面,此时bowl的值就由1变为了2
图解如下:
所以要解决此问题,只需循环判断,将if改为while即可,此时将程序跑起来,结果如下:
出现这种情况,说明是极端情况,所有的执行流PCB都进入PCB等待队列中进行等待,所以程序就不再往下执行,要想解决此问题,给吃面的线程和做面的线程各配一个单独的条件变量,只需两个等待队列,吃面的人通知做面的等待队列,做面的人通知吃面的等待队列
图解如下:
代码如下:
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4
5 #define THREAD_NUM 2
6 int g_bowl = 1;
7
8 pthread_mutex_t My_lock;
9 //eat的条件变量
10 pthread_cond_t g_cond;
11 //make的条件变量
12 pthread_cond_t g_make_cond;
13
14 //Eat
15 void* MythreadA(void* arg)
16 {
17 while(1)
18 {
19 pthread_mutex_lock(&My_lock);
20
21 //判断是否能吃面
22 while(g_bowl < 1)
23 {
24 //等待
25 pthread_cond_wait(&g_cond,&My_lock);
26 }
27 printf("i eat %d,i am %p\n",g_bowl,pthread_self());
28
29 g_bowl--;
30
31 pthread_mutex_unlock(&My_lock);
32 //通知做面
33 pthread_cond_signal(&g_make_cond);
34
35 }
36 return NULL;
37 }
38
39 //Make
40 void* MythreadB(void* arg)
41 {
42 while(1)
43 {
44 pthread_mutex_lock(&My_lock);
45
46 //判断是否需要做面
47 while(g_bowl > 0)
48 {
49 //等待
50 pthread_cond_wait(&g_make_cond,&My_lock);
51 }
52
53 g_bowl++;
54 printf("i make %d,i am %p\n",g_bowl,pthread_self());
55
56
57 pthread_mutex_unlock(&My_lock);
58 //通知吃面
59 pthread_cond_signal(&g_cond);
60
61 }
62 return NULL;
63 }
64
65 int main()
66 {
67 //锁初始化
68 pthread_mutex_init(&My_lock,NULL);
69 //初始化条件变量
70 pthread_cond_init(&g_cond,NULL);
71 pthread_cond_init(&g_make_cond,NULL);
72
73 pthread_t tid_Eat[THREAD_NUM],tid_Make[THREAD_NUM];
74
75 //创建线程
76 for(int i = 0;i < THREAD_NUM;i++)
77 {
78 int ret = pthread_create(&tid_Eat[i],NULL,MythreadA,NULL);
79 if(ret < 0)
80 {
81 perror("pthread_create");
82 return 0;
83 }
84 pthread_create(&tid_Make[i],NULL,MythreadB,NULL);
85 if(ret < 0)
86 {
87 perror("pthread_create");
88 return 0;
89 }
90
91 }
92
93 //线程等待
94 for(int i = 0;i < THREAD_NUM;i++)
95 {
96 pthread_join(tid_Eat[i],NULL);
97 pthread_join(tid_Make[i],NULL);
98 }
99
100 pthread_mutex_destroy(&My_lock);
101 pthread_cond_destroy(&g_cond);
102 pthread_cond_destroy(&g_make_cond);
103 return 0;
104 }
程序跑起来如下:
3.条件变量关于等待接口的几个问题
【问题1】条件变量的等待接口参数为什么需要互斥锁?
由于需要在pthread_cond_wait函数内部进行解锁,解锁之后,其他的执行流才能获取到这把互斥锁,所以,需要传入互斥锁,否则,如果在调用pthread_cond_wait线程在进行等待的时候,不释放互斥锁,其他线程也不会获取到互斥锁,程序就没有办法继续运行了
图解如下:
【问题2】在调用条件变量等待接口的时候,pthread_cond_wait函数的实现逻辑是什么?
(1)先将执行流的PCB放到参数cond的PCB等待队列
(2)释放互斥锁
(3)等待被唤醒
如下用反例来证明
假设,做面的线程判断碗里有面,要进行等待,若它先释放互斥锁,在它释放完互斥锁还未进入等待队列之前,可能吃面的线程拿到了互斥锁,并吃完了面,通知PCB等待队列,而此时做面的线程,还未进入PCB等待队列,PCB等待队列此事为空,吃面的线程又拿到锁去吃面,此时,吃面的线程判断碗里没有面,则将自己放入PCB等待队列中进行等待,而此时做面的线程也在PCB等待队列中进行等待,所以不能先释放互斥锁
反例图解如下:
所以,pthread_cond_wait函数的实现逻辑是,先将执行流的PCB放到参数cond的PCB等待队列,然后再释放互斥锁
【问题3】如果一个线程在等待的时候,被唤醒之后,需要做什么事情?
(1)移动出PCB等待队列
(2)抢互斥锁
- 抢到了:pthread_cond_wait函数返回了
- 没抢到:pthread_cond_wait函数没有返回,等待抢锁