Linux 线程同步

前言

上一期我们介绍了线程互斥,并通过加锁解决了多线程并发访问下的数据不一致问题!本期我们来介绍一下同步问题!

目录

前言

一、线程同步

[• 线程同步的引入](#• 线程同步的引入)

[• 同步的概念](#• 同步的概念)

理解同步和饥饿问题

[• 条件变量](#• 条件变量)

理解条件变量

[• 同步的相关操作](#• 同步的相关操作)

条件变量的创建与销毁

等待条件

唤醒线程

简单的同步测试用例


一、线程同步

• 线程同步的引入

上一期加锁之后的抢票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我们下期再见!

相关推荐
糖豆豆今天也要努力鸭17 分钟前
torch.__version__的torch版本和conda list的torch版本不一致
linux·pytorch·python·深度学习·conda·torch
烦躁的大鼻嘎26 分钟前
【Linux】深入理解GCC/G++编译流程及库文件管理
linux·运维·服务器
ac.char33 分钟前
在 Ubuntu 上安装 Yarn 环境
linux·运维·服务器·ubuntu
敲上瘾33 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
长弓聊编程1 小时前
Linux系统使用valgrind分析C++程序内存资源使用情况
linux·c++
cherub.1 小时前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
梅见十柒1 小时前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热1 小时前
路由基础(全)
linux·网络·网络协议·安全
传而习乎2 小时前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
入 梦皆星河2 小时前
在 Ubuntu/Debian 上安装 Go
ubuntu·golang·debian