文章目录
- 一、线程互斥,它是对的,但是不合理(饥饿问题)——同步
- 二、条件变量
- (一)概念
- (二)条件变量接口
- 1. pthread_cond_init 创建条件变量
- 2. pthread_cond_wait 等待条件满足
- 3. pthread_cond_destroy 销毁条件变量
- 4. 唤醒等待
- (三)例子
- 三、生产者消费者模型
- (一)321模型
- (二)为何要使用生产者消费者模型
- (三)生产者消费者模型优点
- (四)基于BlockingQueue的生产者消费者模型
- 1. 阻塞队列概念
- 2. BlockingQueue阻塞队列代码
- (1)pthread_cond_wait(&conCond_, &mutex_); 解锁
- (2)生产者push时用while循环判断
- 三、POSIX信号量
- (一)概念
- (二)信号量接口
- 1. sem_init 初始化一个未命名的信号量
- 2. sem_destroy 销毁一个信号量
- 3. sem_wait 使用信号量(占座)
- 4. sem_post 归还信号量(退座)
- 三、环形队列
- (一)信号量的作用
- 1.数据为空:消费者不能超过生产者一>生产者先运行
- 2.数据为满:生产者不能把消费者套一个圈然后继续再往后写入——消费者先运行
- (二)code
一、线程互斥,它是对的,但是不合理(饥饿问题)——同步
不合理:互斥有可能导致饥饿问题——由于执行流1优先级高,她就不断的申请锁,释放锁,则另一个执行流2会长时间得不到某种资源。
在保证临界资源安全的前提下(互斥等),让线程访问某种资源,具有一定的顺序性,称之为同步。
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解同步能够解决线程互斥不合理性的问题:防止饥饿,线程协同。
二、条件变量
(一)概念
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
条件(对应的共享资源的状态,程序员要判断资源是否满足自己操作的要求,为满/为空就不满足),条件变量(条件满足或者不满足的时候,进行wait或signal–种方式)。
条件变量:通过判断条件是否满足要求来决定是否让当前线程等待。
(二)条件变量接口
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:条件变量的属性设为空 -
int pthread_cond_destroy(pthread_cond_t *cond);
销毁条件变量
2. pthread_cond_wait 等待条件满足
让对应的线程进行等待,等待被唤醒,即调用这个接口线程会被阻塞。
条件变量要和mutex互斥锁,一并使用,为什么?
条件变量的wait中需要传入锁的意义是:在阻塞线程的时候,会自动释放mutex_锁。
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
3. pthread_cond_destroy 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond)
4. 唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒一个在指定条件变量下等待的所有线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒一个在指定条件变量下等待的线程,一个一个唤醒时,所有线程以队列方式排列的
(三)例子
#include<vector>
#include<iostream>
#include<pthread.h>
#include<functional>
#include<unistd.h>
using namespace std;
pthread_cond_t cond;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
vector<function<void()>> funcs;
void show() {
cout<<"hello show"<<" thread:"<<pthread_self()<<endl;
}
void print() {
cout << "hello print" << endl;
}
void* waitCommand(void* args) {
pthread_detach(pthread_self());
//cout<<"!!!!!!!111111"<<endl;
while(true)
{
pthread_cond_wait(&cond,&mutex);
for(auto& f:funcs)
{
f();
}
}
cout<<"thread id: "<<pthread_self()<<" end... "<<endl;
return nullptr;
}
int main() {
funcs.push_back(show);
funcs.push_back(print);
funcs.push_back([](){
cout<<"你好,条件变量!"<<endl;
});
pthread_cond_init(&cond,nullptr);
pthread_t t1,t2,t3;
pthread_create(&t1,nullptr,waitCommand,nullptr);
pthread_create(&t2,nullptr,waitCommand,nullptr);
pthread_create(&t3,nullptr,waitCommand,nullptr);
while(true) {
sleep(1);
pthread_cond_broadcast(&cond);
}
pthread_cond_destroy(&cond);
return 0;
}
三、生产者消费者模型
生产者消费者模型——同步与互斥的最典型的应用场景——重新认识条件变量
(一)321模型
- 生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者( 互斥/同步)——3种关系
- 生产者和消费者——由线程承担的 2种角色
- 超市:内存中特定的一种内存结构(数据结构)——1个交易场所
这里的仓库就是缓冲区,也是临界资源:①提高效率。②解耦生产者和消费者之间的耦合关系。
(一般就是内存中的一段空间,可以有自己的组织方式) - 消费者有多个,消费者之间是竞争关系——互斥关系
- 生产者有多个,生产者之间也是竞争关系——互斥关系
- 消费者和生产者之间又是什么关系呢?
1.互斥关系:生产者把"123456789"写入缓冲区时(正在生产),消费者突然来拿,可能只拿走了"12345"就错误了,所以消费者和生产者之间也要有互斥关系。
2.同步关系:要有一定的顺序去执行——消费完了要生产,生产满了要消费
(二)为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
(三)生产者消费者模型优点
- 解耦
- 支持并发
- 支持忙闲不均
(四)基于BlockingQueue的生产者消费者模型
1. 阻塞队列概念
在多线程编程中阻塞队列 (Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。
其与普通的队列区别在于:
- 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
- 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出( 以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞),管道就是一种阻塞队列。
2. BlockingQueue阻塞队列代码
(1)pthread_cond_wait(&conCond_, &mutex_); 解锁
挂起时生产者一定是在临界区中的,因为你要先上锁,不上锁就没有资格访问临界资源(如果不上锁就访问临界资源,则不符合同步的规则,那就是代码写的有问题)。此时上锁后被挂起,生产者和消费者用的同一把锁,如果 生产者/消费者 不释放锁(解锁)那么对方的线程就永远无法访问,所以条件变量的wait中需要传入锁的意义是:在阻塞线程的时候,会自动释放mutex_锁(解锁)
(2)生产者push时用while循环判断
while (isFull()):原因是 proBlockWait();调用的 pthread_cond_wait(&proCond_, &mutex_); 有可能返回失败,或被伪唤醒(伪唤醒可能是系统造成的或者写的代码有错误造成)。
如果是 if (isFull()) 则返回失败/伪唤醒后 就直接向下指向条件满足的代码,可是此时阻塞队列还是满的,再添加数据就错了;
所以用while (isFull()) 返回失败/伪唤醒后需要继续判断是否为满,为满就是返回失败/伪唤醒导致的—>重新等待;不满就是等待成功,消费者也消费了—>就可以添加数据了
三、POSIX信号量
一份公共资源,但是允许同时访问不同的区域!
不同的线程并发访问公共资源不同的区域!
(一)概念
POSIX信号量和 SystemV 信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的, 但 POSIX 可以用于线程间同步。
信号量:是一个计数器!
只要拥有信号量,就在未来一定能够拥有临界资源的一部分!
申请信号量的本质:对临界资源中特定小块资源的 <预定> 机制。
例子: 就和你在电影院买票一样,
你买了票,等于你预定了座位!!!
如果你没有买票,就进去坐着,这样是不符合规定的!
只要申请成功,就一定有你得资源!
只要申请失败,就说明条件不就绪,你只能等!
信号量的PV操作:V ++归还资源,P --申请资源。信号量的作用:限制进入临界区的线程个数。
线程要访问临界资源中的某一块区域 ——> 申请信号量 —— > 得先看到信号量 ——> 信号量本身必须是:公共资源
信号量的PV操作:
sem – ; 申请资源 —— 必须保证操作的原子性 psem ++ ; 归还资源 ——必须保证操作的原子性 v
(二)信号量接口
1. sem_init 初始化一个未命名的信号量
man 3 sem_init
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:初始化的信号量。
pshared:0 表示线程间共享,非零表示进程间共享(填0)。
value :信号量初始值
2. sem_destroy 销毁一个信号量
int sem_destroy(sem_t *sem);
3. sem_wait 使用信号量(占座)
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); // P --;
4. sem_post 归还信号量(退座)
功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加1 。
int sem_post(sem_t *sem); // V ++
上面生产者 - 消费者的例子是基于 queue 的 , 其空间可以动态分配 , 现在基于固定大小的环形队列重写这个程序 (POSIX 信号量)。
三、环形队列
(一)信号量的作用
后续操作基本原则:(信号量保证满数据情况只能是消费线程先消费数据资源,数据为空的情况下生产线程先申请空间资源)。
环形队列有可能访问同一个位置。什么时候会发生?
我们两个指向同一个位置的时候只有满or空的时候! ( 互斥and同步)其他时候,都指向不同的位置! (并发 )。
1.数据为空:消费者不能超过生产者一>生产者先运行
生产者:最关心的是什么资源:空间默认是N: [N, 0]
2.数据为满:生产者不能把消费者套一个圈然后继续再往后写入——消费者先运行
消费者:最关心的是什么资源:数据默认是0 [0,N]