目录
- 1.线程同步
- 1.同步概念与竞态条件
- 2.条件变量
- 2.条件变量函数
- 1.初始化 -- pthread_cond_init()
- 2.销毁 -- pthread_cond_destroy()
- 3.等待条件变量 -- pthread_cond_wait()
- 4.唤醒等待
- 5.为什么pthread_cond_wait()需要互斥量?
- 6.错误的程序设计
- 7.条件变量使用规范
- 3.生产者消费者模型
- 1.基本概念
- 2.模型特点
- 3.思考问题
- 4.模型优点
- 4.基于BlockingQueue的生产者消费者模型
- 1.基本概念
- 2.注意点
1.线程同步
1.同步概念与竞态条件
- 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
- 竞态条件: 因为时序问题,而导致程序异常
- 单独使用互斥容易导致饥饿问题,为了解决此问题引入了同步:
- 首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做
- 在我们看来这个线程就一直在申请锁和释放锁
- 这就可能导致其他线程长时间竞争不到锁,引起饥饿问题
- 单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源
- 现在增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后
- 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程
- 如果有十个线程,此时就能够让这十个线程按照某种次序进行临界资源的访问
- 首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做
2.条件变量
- 条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述
- 条件变量主要包括两个动作
- 一个线程等待条件变量的条件成立而被挂起
- 另一个线程使条件成立后唤醒等待的线程
- 条件变量通常需要配合互斥锁一起使用
- 条件变量使唤醒线程由 系统唤醒 --> 让程序员自己唤醒线程
2.条件变量函数
1.初始化 – pthread_cond_init()
- 静态分配:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
- 动态分配:
- 原型:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 参数:
- **cond:**需要初始化的条件变量
- **attr:**初始化条件变量的属性,一般设置为nullptr即可
- **返回值:**成功返回0,失败返回错误码
- 原型:
2.销毁 – pthread_cond_destroy()
- 原型:
int pthread_cond_destroy(pthread_cond_t *cond);
- 参数:
- cond**:**需要销毁的条件变量
- **返回值:**成功返回0,失败返回错误码
- 注意:使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁
3.等待条件变量 – pthread_cond_wait()
- 原型:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 参数:
- cond**:**需要等待的条件变量
- mutex**:**当前线程所处临界区对应的互斥锁
- **返回值:**成功返回0,失败返回错误码
- 注意:wait一定要在加锁和解锁之间进行wait!
4.唤醒等待
- 原型:
int pthread_cond_broadcast(pthread_cond_t *cond);
- **功能:**唤醒等待队列中的全部线程
- 原型:
int pthread_cond_signal(pthread_cond_t *cond);
- **功能:**唤醒等待队列中的首个线程
- 参数:
- **cond:**需要等待的条件变量
- **返回值:**成功返回0,失败返回错误码
5.为什么pthread_cond_wait()需要互斥量?
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足
- 所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
- 条件不会无缘无故的突然变得满足,必然会牵扯到共享数据的变化
- 所以一定要用互斥锁来保护
- 没有互斥锁就无法安全的获取和修改共享数据
- 当线程进入临界区时需要先加锁,然后判断内部资源的情况
- 若不满足当前线程的执行条件,则需要在该条件变量下进行等待
- 但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被释放了,此时就会发生死锁问题
- 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入
- 此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁
- 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁
- 因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁
- 总结:
- 等待条件满足的时候往往是在临界区内等待的
- 当该线程进入等待的时候,互斥锁会自动释放
- 而当该线程被唤醒时,又会自动获得对应的互斥锁
- 条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的
- pthread_cond_wait函数有两个功能,一就是让线程在特定的条件变量下等待,二就是让线程释放对应的互斥锁
- 等待条件满足的时候往往是在临界区内等待的
6.错误的程序设计
pthread_mutex_lock(&mutex);
while (condition_is_false)
{pthread_mutex_unlock(&mutex);// 解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过pthread_cond_wait(&cond);pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
- 由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号
- 那么pthread_cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait
- 所以解锁和等待必须是一个原子操作
- 实际进入pthread_cond_wait()后,会先判断条件变量是否等于0
- 等于0则说明不满足,此时会先将对应的互斥锁解锁 --> 互斥量变成1
- 直到函数返回时再将条件变量改为1,并将对应的互斥锁加锁 --> 互斥量变为0
7.条件变量使用规范
- 等待条件代码
- 为什么使用while循环? --> 怕发生虚假唤醒
pthread_mutex_lock(&mutex);
while (条件为假)pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
- 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
3.生产者消费者模型
1.基本概念
- 生产者消费者模式就是通过一个容器(阻塞队列)来解决生产者和消费者的强耦合问题
- 生产者和消费者彼此之间不直接通讯,而通过这个容器来通讯
- 所以生产者生产完数据之后不用等待消费者处理,直接将生产的数据放到这个容器当中
- 消费者也不用找生产者要数据,而是直接从这个容器里取数据
- 这个容器就相当于一个缓冲区,平衡了生产者和消费者的处理能力
- 这个容器实际上就是用来给生产者和消费者解耦的
- 让消费者和生产者协同工作,合适的时候可能一直运行
- 生产者和消费者并不会因为要互相等待对方的结果而阻塞,相当于双方可以并发执行
- 生产者和消费者并不会因为要互相等待对方的结果而阻塞,相当于双方可以并发执行
2.模型特点
- 总结为321原则
- 3种关系:
- 生产者和生产者(互斥关系)
- 消费者和消费者(互斥关系)
- 生产者和消费者(互斥关系、同步关系)
- 2****种角色: 生产者和消费者
- 1****个场所: 通常指的是内存中的一段缓冲区
- 3种关系:
- 注意:
- 互斥关系保证的是数据的正确性
- 同步关系是为了让多线程之间协同起来
- 编写生产者消费者模型的时候,本质就是对这三个特点进行维护
3.思考问题
-
生产者和生产者、消费者和消费者、生产者和消费者,它们之间为什么会存在互斥关系?
- 介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来
- 其中,所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系
-
生产者和消费者之间为什么存在同步关系?
- 如果让生产者一直生产,那么当生产者生产的数据将容器塞满后,生产者再生产数据就会生产失败。
- 反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败。
- 虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的
- 应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费
-
为什么要有生产者消费者模型?
- 本质是用代码进行解耦的过程
4.模型优点
- 解耦
- 支持并发
- 支持忙闲不均 --> 哪边的线程忙可以多分配一些线程
- 如何理解解耦?
- 如果在主函数中调用某一函数,那么必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合
- 对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合
4.基于BlockingQueue的生产者消费者模型
1.基本概念
- 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构
- 其与普通的队列区别:
- 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素
- 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出
2.注意点
- 判断是否满足生产消费条件时不能用if,而应该用while
- pthread_cond_wait函数是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行,就会出现错误(没有数据还拿,没有空间还放)。
- 在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了。
- 为了避免出现上述情况,就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件
- 因此这里必须要用while进行判断