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我们下期再见!

相关推荐
A小辣椒7 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒11 小时前
TShark:基础知识
linux
AlfredZhao13 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言