前言
上一期我们介绍了线程互斥,并通过加锁解决了多线程并发访问下的数据不一致问题!本期我们来介绍一下同步问题!
目录
[• 线程同步的引入](#• 线程同步的引入)
[• 同步的概念](#• 同步的概念)
[• 条件变量](#• 条件变量)
[• 同步的相关操作](#• 同步的相关操作)
一、线程同步
• 线程同步的引入
上一期加锁之后的抢票Demo中,我们发现虽然不会出现多卖出票的情况了,但是我么发现一个线程可以连续抢到很多的票,搞得我们有些线程像是黄牛了~!
上面某个线程一直执行抢票动作,而抢票的过程是互斥的,也就是加了锁的!在它抢票期间,其他的线程是一直得阻塞等待的,如果那些阻塞等待的线程一直竞争不到锁,就会造成线程长时间无法被调度的饥饿问题 !为了解决上述的黄牛式的抢票的问题,让其他的线程有机会被调度!我们就引入了线程的同步~!
• 同步的概念
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题的机制,叫做同步!
竞态条件:因为时序问题而导致程序出现异常
理解同步和饥饿问题
OK,我们直接上一个例子解释:
话接上回,张三早上6:00就欢快的抢到VIP自习室的钥匙,进去自习了!过了一会,来了好多想要进VIP自习室自习的人!但是由于张三没出来,他们没有钥匙就进不去!
他们都在等着张三出来归还钥匙,但是张三不急不慢的自习到中午的12:00,此时他有点饿了,想出去吃个饭,出去吃饭就意味着仔细结束,得把钥匙归还!张三刚到门口把钥匙挂到门上,回头一看这么多等待的人,张三一想,我这把钥匙挂上去,吃个饭回来,我就是这些等待的人中的一个,又不知道到得等到啥时候~!所以张三心一横,又把钥匙拿了下来,又进去学习了;刚进去没几分钟张三就饿得不行了,于是去把钥匙挂到门上,回头一看这么多的人就又拿下来,强撑着进去了!就这样张三反复进出,直到晚上的8 : 00 ,张三自己不仅没有吃上饭、没专心的学习,而且还导致其他人也无法进入自习室学习!
但是按照规定张三并没有错 ,且符合自习室只允许持有钥匙的一人进入的规则!只不过他的这种行为极其不合理 (现实中估计家人不保)!因为张三这种不合理的行为,导致了自习室的资源浪费 ,其他同学没机会进VIP自习室自习,从而陷入了 饥饿状态!为此,管理员连夜修改了规则:
• 在外面等待的同学必须排队等待
• 所有自习完的同学在归还玩钥匙之后,不能立即申请,下次申请,需要排队
新规则出来以后,自习室的使用都是按照一定的顺序 进行申请使用,再也没有了以前的饥饿问题!上述的旧规则时期每张三反复进出的,导致其他人长时间不能进入自习室,就是导致饥饿问题!新规则后多人按照一定顺序申请使用自习室,就是同步!
• 条件变量
原生的线程库 提供了 条件变量 来实现 线程同步
通过条件变量 -> 实现新线程同步 -> 解决了饥饿问题
•条件变量 :当一个线程互斥的访问某个变量时,他可能发现在其他线程改变状态之前,什么也做不了
比如当一个线程访问队列时,发现队列为空,它只能等待,直到其他线程往队列中添加数据,此时就可以考虑使用 条件变量
理解条件变量
现在有A和B两人协作 的一个拿放苹果游戏:
规则是A得蒙着眼睛,B不能说话!B向盘子放苹果,A从盘子拿苹果;盘子任意时刻只能由一人使用,最终哪个组拿的最多就获胜,奖励一个iphone16;
有很多组参加,你(李四)和你的搭档张三,想得第一名,整个iphone玩一玩!于是就报名参加了结果上半场,由于你不知道盘子是否有苹果而频繁的申请盘子,检测盘子;导致了你的搭档张三根本就无法拿到盘子,更别说放苹果了!所以,上半场以0个苹果结束!
在中场休息的时间,你很仔细阅读了规则,发现没说不能使用工具,所以,你俩就整了个铃铛,张三给你说,李四啊,你如果拿到盘子没有苹果,你就把盘子还回去,不要再拿了,你就定定的等着,当我把苹果放到盘子了,就敲响一声铃铛,你就来拿,你拿完继续等着!果然下半场,利用这个机制,你们拿下了很多的苹果!
但是,下半场结束后,有一组和你们的苹果数一样的多,所以有增加了一场,但是这一场的规则稍有变化,就是给每一组增加一位拿苹果的人,看最后哪一组拿的多,就谁赢!
有了上半场的经验,你和张三以及新队友王五,一起提前开会,你们说好了,由于多了一个人,这次铃铛的规则也变了,当敲一声时一个人来拿,当敲两声时,都来拿(但实际只有一个人能拿到盘子,所以两人得竞争,得拼手速)!于是就开始了,但这次显然你们比对手准备的好,你们最后比他们拿的多!
上述的盘子就是临界资源 ,一次只能一个人那盘子就是互斥 ,铃铛就是条件变量!此时,当你的搭档不敲铃铛时,你啥也做不了,就在那等着!
条件变量的本质可以理解为 衡量或指示访问资源状态的一种机制。
所以,条件变量内部必须实现两个东西:1、需要一个等待队列 2、需要一个通知机制!
• 同步的相关操作
条件变量的创建与销毁
条件变量 和互斥锁 都是原生线程库 中的,他的接口风格 和互斥锁极其相似,例如:
互斥量 的类型: pthread_mutex_t 条件变量 的类型: pthread_cond_t
定义全局的条件变量
cpp
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
注意 :在全局创建条件变量时,初始化为 PTHREAD_COND_INITIALIZER,自动销毁!
定义局部的条件变量
cpp
#include <pthread.h>
pthread_cond_t cond; // 定义一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数
restrict cond 表示要初始化的条条件变量
restrict attr 表示初始化时的相关属性,直接设置为nullptr即可
返回值
成功,返回0
失败,返回错误码
注意**:这些接口的返回值都是一样的,后续不再介绍!**
条件变量的销毁
cpp
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
参数
cond 表示要销毁的条件变量
等待条件
cpp
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数
cond :表示要等待的条件变量
restrict mutex :互斥锁,用于辅助的条件变量
为什么等待时需要一把互斥锁?
1、条件变量也是临界资源,也需要被保护
2、当条件不满足时,需要将线程挂起到特定的阻塞队列,但是其已持有锁资源,为了避免死锁,条件变量内部再把该线程挂起前,要对该锁资源进行释放,会用到它!
唤醒线程
当条件变量满足时,需要唤醒阻塞队列中等待该条件变量的线程,可以唤醒一个,也可以唤醒全部,这就是我们上述所说的通知机制!
cpp
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有
第一个表示唤醒等待条件变量阻塞队列的队头的那一个线程,第二个时唤醒阻塞队列中所有线程!
注意:当全部唤醒之后,所有的线程也会先去竞争锁,如果持有锁了继续后续的操作,如果竞争失败了,去锁那里等待锁资源!
简单的同步测试用例
我们写一个小Demo,让num个线程都在进入临界区之后等待,主线程唤醒之后再去执行!
cpp
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 创建一个全局的条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 创建一个全局的互斥量
void *Wait(void *args)
{
const char *name = static_cast<char *>(args);
while (true)
{
// 加锁
pthread_mutex_lock(&mutex);
// 让所有线程一进来就再cond的条件下等待
pthread_cond_wait(&cond, &mutex);
std::cout << name << ", 正在运行....." << std::endl;
// 解锁
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
pthread_t tid[num];// 启动5个线程
for(int i = 0; i < num; i++)
{
char* name = new char[128];
snprintf(name, 128, "thread_%d", i+1);
pthread_create(tid+i, nullptr, Wait, (void*)name);//创建5个线程
}
sleep(3);//d等所有的线程起来
// 主线程唤醒新线程
while(true)
{
std::cout << "Main wake up new thread: " << std::endl;
pthread_cond_signal(&cond);// 唤醒一个
}
// 等待线程
for(int i = 0; i < num; i++)
{
pthread_join(*(tid+i), nullptr);
}
return 0;
}
OK,此时是主线程一次唤醒一个线程,我们看看效果:
我们再来试试全部唤醒:
OK,没有问题!好兄弟我是cp我们下期再见!