目录
1、对于生产者与消费者来说,获取信号量与加锁的先后顺序是怎样的?
死锁
死锁的产生场景
1、线程加锁之后,没有释放互斥锁就退出了
因此,要在线程所有可能退出的地方都释放互斥锁。
2、两种线程分别拿着一把锁,还想要请求对方的锁
死锁的gdb分析
产生了死锁,要分析其产生的原因,可以通过gdb来调试该程序,进而判断思索地原因。下面以上述情况2进行调试。
1、通过调试可执行程序来分析
1、b + 行号 打断点
2、使用thread apply all bt命令将所有线程的调用堆栈展现出来
3、使用p + 互斥锁变量可以查看互斥锁的内容
2、通过调试正在运行的程序
gdb attack + 进程号
此时就可以进入gdb调试当中,我们可以按照上述查看线程调用堆栈以及查看互斥锁的内同等命令来分析我们的代码。
死锁的必要条件
1、不可剥夺
线程获取到互斥锁后,除了自己释放,其他线程不能进行释放
2、循环等待
线程A拿着1锁请求2锁,线程B拿着2锁请求1锁
3、互斥条件
一个互斥锁在同一时间只能被一个线程拥有
4、请求与保持
吃着碗里的,看着锅里的,其实就是死锁产生的情况2那种情形
死锁的预防
1、破环必要条件
- 破坏循环等待
- 破坏请求与保持
- 剩下的不可剥夺和互斥条件是锁的固有属性,无法破坏
2、加锁顺序一致
都先加同一把锁,再去加另一把锁
3、避免锁没有被释放
在线程所有退出的地方都进行解锁
4、资源一次性分配
多个资源在代码当中有可能每一个资源都需要使用不同的锁进行保护,比如:资源A需要1锁,资源B需要2锁,就有可能出现多个线程在使用这两个资源的时候,出现循环等待的情况。
比如可以让多个资源共同使用同一把锁,就能达到一次性分配的目的。
生产者与消费者模型
123规则
1个线程安全的队列:保证先进先出的数据结构即可,互斥 + 同步
2种角色的线程:生产者和消费者
3个规则:
- 生产者与生产者之间互斥
- 消费者与消费者之间互斥
- 生产者与消费者互斥 + 同步
应用场景及特点
一般应用于后端程序当中,比如微信的后端程序:
1、接受消息的线程接受消息放到队列中
2、多个线程充当消费者从队列中读取数据并处理3、这多个线程又充当生产者,将处理完毕的结果放到另一个队列中。
4、发送数据的线程从队列中拿数据并发送
优点:
1、忙闲不均
2、生产者与消费者高度解耦
3、支持高并发
代码实现:
1 #include<stdio.h>
2 #include<queue>
3 using namespace std;
4 #include<pthread.h>
5 #include<unistd.h>
6
7 #define THREAD_COUNT 2
8 //线程安全队列
9 class RingQueue{
10 public:
11 RingQueue(){
12 capacity = 10;
13 pthread_mutex_init(&_que_lock, NULL);
14 pthread_cond_init(&_cons_cond, NULL);
15 pthread_cond_init(&_prod_cond, NULL);
16 }
17 ~RingQueue(){
18 pthread_mutex_destroy(&_que_lock);
19 pthread_cond_destroy(&_cons_cond);
20 pthread_cond_destroy(&_prod_cond);
21 }
22 //提供给生产者线程使用的接口
23 void push(int data){
24 pthread_mutex_lock(&_que_lock);
25 while(_que.size() == capacity){
26 pthread_cond_wait(&_prod_cond, &_que_lock);
27 }
28 _que.push(data);
W> 29 printf("I am produce pthread %p: I produce %d\n", pthread_self(), data);
30 pthread_mutex_unlock(&_que_lock);
31 //通知消费者消费
32 pthread_cond_signal(&_cons_cond);
33 }
34 //提供给消费者线程进行消费的线程
35 void pop(int* data){
36 pthread_mutex_lock(&_que_lock);
37 while(_que.size() == 0){
38 pthread_cond_wait(&_cons_cond, &_que_lock);
39 }
40 *data = _que.front();
41 _que.pop();
W> 42 printf("I am consume thread %p : I consume %d\n", pthread_self(), *data);
43 pthread_mutex_unlock(&_que_lock);
44 //通知生产者线程生产
45 pthread_cond_signal(&_prod_cond);
46 }
47 private:
48 queue<int> _que;
49 size_t capacity;
50 //互斥锁
51 pthread_mutex_t _que_lock;
52 //同步
53 pthread_cond_t _cons_cond;
54 pthread_cond_t _prod_cond;
55 };
56
57 int g_data = 0;
//注意:静态初始化互斥锁保证临界区资源代码的原子性
//不能在该函数内部定义互斥锁变量,因为那样多个生产者拿到的不是一个互斥锁,会造成程序结果
//的二义性
58 pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
59 void* pro_thread_start(void* arg){
60 RingQueue* rq = (RingQueue*)arg;
61 while(1){
62 pthread_mutex_lock(&g_lock);
63 rq->push(g_data);
64 g_data++;
65 //sleep(1);
66 pthread_mutex_unlock(&g_lock);
67 }
68 }
69 void* con_thread_start(void* arg){
70 RingQueue* rq = (RingQueue*)arg;
71 while(1){
72 int data;
73 rq->pop(&data);
74 }
75 }
76 int main(){
77 RingQueue* que = new RingQueue();
78 pthread_t pro[THREAD_COUNT], con[THREAD_COUNT];
79 for(int i = 0; i < THREAD_COUNT; i++){
80 int ret = pthread_create(&pro[i], NULL, pro_thread_start, que);
81 if(ret < 0){
82 perror("pthread_create");
83 }
84 ret = pthread_create(&con[i], NULL, con_thread_start, que);
85 if(ret < 0){
86 perror("pthread_create");
87
88 }
89 }
90 //主线程等待回收工作线程
91 for(int i = 0; i < THREAD_COUNT; i++){
92 pthread_join(pro[i], NULL);
93 pthread_join(con[i], NULL);
94 }
95 delete que;
96 return 0;
97 }
结果:
信号量
原理:
信号量是由一个资源计数器和一个PCB等待队列构成。
PCB等待队列:与条件变量实现同步中的PCB等待队列是一样的。
资源技术器:
执行流获取信号量:
获取成功:资源计数器减一
获取失败:执行流被放到PCB等待队列中去
执行流在释放信号量后,对资源计数器进行+1操作。
生产者信号量初始化时资源资源计数器的值一般为线程安全队列的容量大小。消费者信号量初始化时资源计数器的值一般为,因为刚开始,线程安全队列中并没有资源可以使用。
接口:
初始化接口:
sem:信号量,sem_t是信号量的类型
pshared: 表示该信号量用途的标识符;0用于线程间,是一个全局性质的结构体变量;非0用于进程间,涉及到进程间通信,该变量肯定存在每个进程都能访问到的地方,比如共享内存。
value:资源的个数,用来初始化信号量的资源计数器
等待接口:
1、对资源计数器进行-1操作
2、判断资源计数器的值是否小于0
小于0:阻塞等待,将执行流放到PCB等待队列中
不小于0:接口返回
释放接口:
1、对资源计数器+1操作
2、判断资源计数器的值是否小于等于0
是:通知PCB等待队列(说明有其他线程在PCB等待队列中)
否:不同通知PCB等待队列因为没有线程在等待
销毁接口:
int sem_destroy(sem_t* sem)
信号量是动态初始化的,因此需要销毁
注意事项:
1、对于生产者与消费者来说,获取信号量与加锁的先后顺序是怎样的?
场景:有一个容量为2的线程安全队列,有3个生产者线程ABC,生产者对应的信号量中资源计数器初始值为2,有一个消费者,消费者资源计数器初始值为0。
场景一:先拿互斥锁,再获取信号量
假设生产者线程A先拿到互斥锁 ,然后去获取信号量,并对生产者的资源计数器-1,此时值变为1;然后线程A去访问线程安全队列这一临界资源,访问完毕后,线程A释放信号量,并对消费者信号量的资源计数器+1,此时消费者资源计数器为1。
假设接下来又是生产者线程B拿到互斥锁,然后去获取信号量,此时生产者资源计数器变为0,经过一系列操作后,消费者资源计数器的值变为2。
此时,线程安全队列中已经没有空闲空间可以使用了。
假设生产者C又拿到了互斥锁,然后去获取信号量,并对生产者的资源计数器-1,现在资源计数器小于0.因此阻塞等待,将线程C放到生产者的PCB等待队列中去。
此时,线程C带着互斥锁进到了PCB等待队列等待其他线程的唤醒。但是其他线程并不能获取到互斥锁,因而也就无法唤醒该线程。整个线程处于卡死状态,无法继续向下执行。
因此这种情况是错误的。
场景二:先获取信号量,再获取互斥锁
对于信号量的获取,线程ABC是抢占式获取的,再获取到信号量修改计数器是原子性的。
假设AB先获取到信号量并对资源计数器减一,此时计数器的值为0。对于获取到信号量的AB,假设线程A先获取到互斥锁。然后访问临界资源,释放信号量,对消费者资源计数器+1。
假设接下来又是生产者线程B拿到互斥锁,此时整个线程安全队列已经放满。
接下来由消费者线程获取到互斥锁,然后消费者线程再对临界区访问完毕后,释放信号量,对生产者的资源计数器+1,发现值小于等于0,因此会通知生产者的PCB等待队列,然后线程就去直接获取互斥锁。
因此是先获取信号量,再保证互斥(方式:互斥锁 || 信号量)
2、信号量既可以保证同步,也可以保证互斥
信号量保证同步,再分析上一个问题的时候已经体现出来了。
而保证互斥只需要将信号量中资源计数器的初始值设置为1,就能够保证互斥了。
将资源计数器的值设置为1,也就意味着只有一个线程能够获取到信号量,其他线程再获取的时候,资源计数器的值小于0,他们都会被放到该信号量的PCB等待队列中去;只有等到获取到信号量的那个线程访问临界资源完成,将该信号量释放后,其他线程才能够获取到该信号量,进而访问临界资源。
使用信号量来实现生产者与消费者模型
使用一个环形队列作为线程安全队列,该环形队列使用一个数组来模拟:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4 #include<vector>
5 #include<semaphore.h>
6
7 using namespace std;
8 /*
9 * 定义线程安全的队列
10 * 环形队列(用数组模拟)
11 * 线程安全:
12 * 同步:信号量
13 * 互斥: 信号量
14 * */
15 #define CAPACITY 2
16 #define THREAD_COUNT 2
17
18 class RingQueue{
19 public:
20 RingQueue()
21 :_vec(CAPACITY){
22 _capacity = CAPACITY;
23 sem_init(&_sem_lock, 0, 1);
24 sem_init(&_cons_sem, 0, 0);
25 sem_init(&_prod_sem, 0, CAPACITY);
26 write_pos = 0;
27 read_pos = 0;
28 }
29 ~RingQueue(){
30 sem_destroy(&_sem_lock);
31 sem_destroy(&_cons_sem);
32 sem_destroy(&_prod_sem);
33 }
34 void push(int data){
35 //获取生产者信号量
36 sem_wait(&_prod_sem);
37 //获取互斥锁
38 sem_wait(&_sem_lock);
W> 39 printf("I am produce thread %p: I produce %d\n", pthread_self(), data);
40 _vec[write_pos] = data;
41 write_pos = (write_pos + 1) % _capacity;
42 sem_post(&_sem_lock);
43 //通知消费者进行消费
44 sem_post(&_cons_sem);
45 }
46 void pop(){
47 //获取消费者信号量
48 sem_wait(&_cons_sem);
49 sem_wait(&_sem_lock);
50 int data = _vec[read_pos];
W> 51 printf("I am consum thread %p: I consum %d\n", pthread_self(), data);
52 read_pos = (read_pos + 1) % _capacity;
53 sem_post(&_sem_lock);
54 //通知生产者生产
55 sem_post(&_prod_sem);
56 }
57 private:
58 vector<int> _vec;
59 size_t _capacity;
60 //保证互斥的信号量
61 sem_t _sem_lock;
62 sem_t _cons_sem; //消费者的信号量
63 sem_t _prod_sem; //生产者的信号量
64
65 int write_pos;
66 int read_pos;
67 };
68 int g_data = 0;
69 sem_t g_lock;
70 void* prod_thread_start(void* arg){
71 RingQueue* rq = (RingQueue*)arg;
72 while(1){
73 sem_wait(&g_lock);
74 rq->push(g_data);
75 g_data++;
76 sem_post(&g_lock);
77 }
78 }
79 void* cons_thread_start(void* arg){
80 RingQueue* rq = (RingQueue*)arg;
81 while(1){
82 rq->pop();
83 }
84 }
85 int main(){
86 RingQueue* rq = new RingQueue();
87 //保证多个生产者互斥访问
88 sem_init(&g_lock, 0, 1);
89
90 pthread_t cons[THREAD_COUNT], prod[THREAD_COUNT];
91 for(int i = 0; i < THREAD_COUNT; ++i){
92 int ret = pthread_create(&prod[i], NULL, prod_thread_start, (void*)rq);
93 if(ret < 0){
94 perror("pthread_create");
95 }
96 ret = pthread_create(&cons[i], NULL, cons_thread_start, (void*)rq);
97 if(ret < 0){
98 perror("pthread_create");
99 }
100 }
101 for(int i = 0; i < THREAD_COUNT; ++i){
102 pthread_join(prod[i], NULL);
103 pthread_join(cons[i], NULL);
104 }
105 sem_destroy(&g_lock);
106 delete rq;
107 return 0;
108 }