Linux->线程同步

目录

前言:

1 线程同步引入

2 条件变量

2.1 线程饥饿

2.2 条件变量接口

2.3 添加条件变量

3 生产者和消费者模型


前言:

        本篇主要讲解了关于线程同步的相关知识,还有生产者和消费者模型的认识和使用。

1 线程同步引入

        在讲解线程同步之前,我们先来看一下当一个程序之中只有线程互斥时会有什么样的问题,这份代码是我上一篇买票程序的改写。

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;


int Ticket = 1000;
#define NUM 5

//初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//买票线程执行
void* buyTicket(void* args)
{
    string name = static_cast<const char*>(args);

    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(Ticket > 0)
        {
            usleep(1000);
            --Ticket;
            cout << name << "购买了一张票,还剩下:" << Ticket<< endl; 
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
        }

        usleep(10);
    }

    return nullptr;
}

//放票线程执行
void* putTicket(void* args)
{
    string name = static_cast<const char*>(args);

    while(1)
    {
        //每十秒钟放1000张票
        sleep(10);
        pthread_mutex_lock(&mutex);
        Ticket+=1000;
        pthread_mutex_unlock(&mutex);
    }
    return 0;
}


int main()
{
    //创建买票线程
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; ++i)
    {
        char* name = new char[64];
        snprintf(name,64,"%s-%d","thread",i+1);
        int n = pthread_create(tids+i,nullptr,buyTicket,name);
        if(n != 0)
        {
            cout << "create thread fail" << endl;
        }
    }

    //创建放票线程
    pthread_t creT;
    char* name = new char[64];
    snprintf(name,64,"%s", "放票线程");
    int n = pthread_create(&creT,nullptr,putTicket,name);
    if(n != 0)
    {
        cout << "create thread fail" << endl;
    }

    //等待
    for(int i = 0; i < NUM; ++i)
    {
        n = pthread_join(tids[i],nullptr);
        if(n != 0)
        {
            cout << "join thread fail" << endl;
        }
    }

    n = pthread_join(creT,nullptr);
    if(n != 0)
    {
        cout << "join thread fail" << endl;
    }

    return 0;
}

        上面的程序当中,我创建了5个线程用于执行买票这个动作,当票买完之后并不退出继续判断,然后每隔十秒钟,我们的放票线程会放出1000张票,然后买票线程继续买票,整个程序的临界资源通过锁来保护。这是上面一段代码的运行逻辑。

        输出为以下结果:

         从结果上来看,我们的程序是正确运行了,但是大家有没有想过一个问题,既然我们的票都已经没有了,我们的线程还一直在哪里进行无意义的死循环,不断地加锁,解锁,访问资源,占用CPU的运行是不是不太好啊?难道就没有一种方式能够让我们的程序只有在有票的时候才去拿,没票的时候就直接等待着吗?

        答案是,当然有,不然你以为我们的线程同步是干嘛的,就是为了防止这种无价值的执行逻辑占用我们的CPU资源,所以也就引出了我们的条件变量这一概念。

2 条件变量

2.1 线程饥饿

        在讲条件变量之前,我得给大伙补充一个知识,那就是线程饥饿问题。线程饥饿从概念上理解就是一个线程处于长时间等待资源,但是申请不到的状态。可以简单理解为另类的“死锁”,但是不是死锁哈。

        什么意思呢?注意到我们的买票不管成功与否,是不是我们都进行了usleep(10)这一句代码呢?我添加他的意义是什么呢?

        假设我们的临界区是一个自习室,而正在执行的线程就是我,这个自习室只能有一个人在里面学习,钥匙也只有一把,不能强夺,这是前提条件。

        今天,我凌晨4点就跑到了自习室里面去了,由于规则的限制,后来的人只能在外面等我出去,然后把钥匙拿出来。这符合锁的逻辑。然后呢,我学着学着饿了,就学不进去,想去干饭,刚一出去,把锁放下,其他人正等着拿这把钥匙,我转念一想“不行,我出去了岂不是说我的自习室没了?”,因为我距离这把钥匙最近,所以我又把他那回到了手里,从回自习室,其它的人只能再继续等待。回去1分钟之后,我又遭不住了,又出去,如此循环操作,整个上午,我任何事情也没有做,只是在这里把钥匙拿起来,放回去,非常的欠揍啊。

        上面我的这个行为肯定是有问题的,但是我做错了什么吗?是不是你说的自习室只能有一个人在里面?是不是你说的想要进入必须拿到教室?是不是你说的不能抢钥匙?我只是啥也没干,就玩,你能说我做错了什么吗?不能,这个例子不就是我们买票程序不加usleep(10)的运行结果吗?

        所以基于这样一个问题,我们能咋做呢?只能添加规则,出自习室之后,放回钥匙,想要重新进入这个自习室必须到最后去排队,之前在自习室外等待的人也必须排队。这样就能方式饥饿问题的产生。那么如何做到的呢?由我们的条件变量来控制。

2.2 条件变量接口

初始化方法:和锁差不多

 等待条件成立:需要配合锁使用,单独用不起作用

         根据线程执行这一段代码的顺序,会根据这个顺序对线程排序等待,并且这个函数能够自动释放锁,并且等待唤醒函数。

 唤醒线程:signal唤醒一个,broadcast唤醒所有

         这一点博主会在后续为大家展示出区别。

2.3 添加条件变量

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;


int Ticket = 1000;
#define NUM 5

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* buyTicket(void* args)
{
    string name = static_cast<const char*>(args);

    while(1)
    {
        pthread_mutex_lock(&mutex);
        while(Ticket <= 0)
        {
            pthread_cond_wait(&cond, &mutex);
        }
        
        if(Ticket > 0)
        {
            usleep(1000);
            --Ticket;
            cout << name << "购买了一张票,还剩下:" << Ticket<< endl; 
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
        }
        usleep(10);
    }

    return nullptr;
}

void* putTicket(void* args)
{
    string name = static_cast<const char*>(args);

    while(1)
    {
        //每十秒钟放1000张票
        sleep(10);
        pthread_mutex_lock(&mutex);
        Ticket+=1000;
        cout << name << "放出了1000张票" << endl;
        //pthread_cond_broadcast(&cond);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);    
    }
    return 0;
}


int main()
{
    //创建买票线程
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; ++i)
    {
        char* name = new char[64];
        snprintf(name,64,"%s-%d","thread",i+1);
        int n = pthread_create(tids+i,nullptr,buyTicket,name);
        if(n != 0)
        {
            cout << "create thread fail" << endl;
        }
    }

    //创建放票线程
    pthread_t creT;
    char* name = new char[64];
    snprintf(name,64,"%s", "放票线程");
    int n = pthread_create(&creT,nullptr,putTicket,name);
    if(n != 0)
    {
        cout << "create thread fail" << endl;
    }

    //等待
    for(int i = 0; i < NUM; ++i)
    {
        n = pthread_join(tids[i],nullptr);
        if(n != 0)
        {
            cout << "join thread fail" << endl;
        }
    }

    n = pthread_join(creT,nullptr);
    if(n != 0)
    {
        cout << "join thread fail" << endl;
    }

    return 0;
}

pthread_mutex_lock(&mutex);

while(Ticket <= 0)

{

      pthread_cond_wait(&cond, &mutex);

}

……运行代码

         注意我是如何添加条件变量等待的?我通过直接放在了锁的下面,然后放在了运行代码的上面,这是为了干什么?因为条件变量就像是一个先决条件一样,他成功了才能允许代码向后执行,而不是代码已经执行完了再来判断这个行为是否需要被处理。

broadcast:

pthread_cond_broadcast(&cond);

运行结果:

signal:

pthread_cond_signal(&cond);

运行结果:

         出现上述两种结果上的差异的原因是由于signal每一次只唤醒一个线程,而我们等待的线程并不只是一个,所以才会出现原来有很多的线程,但是放票之后只有一个线程在跑的情况。

        而broadcast则可以将所有的线程按照循序全部唤醒,注意这个过程一定不是同时的,因为要保证锁的唯一性,也只能是等待释放才会继续唤醒下一个。

3 生产者和消费者模型

        对于生产者和消费者模型其实原理上是非常容易理解的,我们将其转换成为一个现实中的具象物,我们就是消费者,商品厂家就是生产者。也就是厂家生产我们消费,很简单。

        但是大家有没有想到,我们现实当中买一个东西是直接跑到人家厂家哪里去的吗?并不是欸,我们的方式是跑到超市去买,而厂家会直接卖一根火腿肠给我们吗?也不是,他会将大量的货给超市。所以说生产者和消费者模型当中,必然涉及到了一个场所的存在

        第二个问题,某一天我想要去买一根火腿肠,但是超市里面已经没有了,怎么办呢?没办法,没有就是没有,只能回去,但是我又想吃,然后我又跑到超市去了,超市还是没有,这个行为我连续做了1000次,还是没有。这个时候超市的店员就有问题了,这个时候他应该给我一个联系方式,等到有火腿肠的时候再给我打电话让我来买,而不是我一直跑过来问;其次他还应该给厂家通知需要进货了,而不是让场所一直没有商品,也就是需要保证消费者和生产者的同步关系

        第三个问题,超市里面有货了,但是只有一根火腿肠,我和张三都想要这跟火腿肠,但是只有一根,我们打了一架,不分高低,最后对超市造成了不好的影响。这个时候超市就无语了,只好定下一个新的规定,谁先进店谁能优先购买,快一点点也算。这表示了消费者之间需要保持互斥关系

        第四个问题,此时商家打了电话给商家上货之后,同时来了两家商家上货,它们谁也不让谁,非要放到同一个货架里面,放了自己的还把别人的丢了,因此两家争吵不止,于是超市也定下了,谁先到超市谁有优先权上货。这表示了生产者和生产者之间需要保持互斥关系

        第五个问题,一个商家正在上货,我正好要的就是这个产品,然后我就去拿了一些下来,商家不乐意了,我还没结账呢,你这拿了算谁的,然后它反手就给我抢了回来,我服它吗?我不服,所以我又抢回来了,又开始争起来了,没办法,商家只能规定,在上货的时候不能买,在买货的时候不能上货。也就是消费者和生产者要保持一个互斥的关系

        总结起来就是,生产者和消费者模型一共需要一个场所(交易场所),两个角色(生产者和消费者),三种关系(生产者和生产者的互斥关系,消费者和消费者的互斥关系,生产者和消费者的同步与互斥关系)

        我还听说过生产者和消费者模型有高效性哇,这是在哪里体现出来的?维护生产者和消费者各自的关系不是会让多线程并发执行变为单执行流执行吗?这个不是与高效性正好相悖吗?

        我的回答是,提出这个问题的伙伴眼界放窄了,我们要知道并不是说每时每刻都是所有线程在消费,每时每刻都是所有线程在生产,它们有的是正在处理拿到的数据,有的正在拿,有的正在放,有的正在获取数据来源。也就是保证我们的整个进程的所有执行流分别做着自己的事,因为有交易场所的存在,所以说它们会有条不紊的执行自己的事情。这才是高效的真正体现。

C++ queue 模拟阻塞队列的生产消费模型:
#include <iostream>
#include <pthread.h>
#include "mythread.hpp"
#include<unistd.h>
using namespace std;

// 实现一个生产者和消费者模型

// 消费者执行动作
void *comsumer(void *args)
{
    // 类型转换
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);

    while (true)
    {
        sleep(1);
        int data = 0;
        bq->pop(&data);
        cout << "消费者拿到一个数据:"<<pthread_self() << " : " << data << endl; 
    }
}

// 生产者执行动作
void *productor(void *args)
{
    // 类型转换
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);

    while(true)
    {
        int data = rand() % 20;
        bq->push(data);
        cout << "生产者放入一个数据:" << data << endl; 
    }

}


int main()
{

    pthread_t c[2], p[3];
    BlockQueue<int> *bq = new BlockQueue<int>();

    pthread_create(&c[0], nullptr, comsumer, bq);
    pthread_create(&c[1], nullptr, comsumer, bq);
    pthread_create(&p[0], nullptr, productor, bq);
    pthread_create(&p[1], nullptr, productor, bq);
    pthread_create(&p[2], nullptr, productor, bq);

    // 保持生产者和消费者的执行
    while (true)
    {
        ;
    }

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);

    return 0;
}
#pragma once 
#include<iostream>
#include<pthread.h>
#include<queue>
using namespace std;

#define CAP 5

//无法判断我们需要的类型是什么,添加模板参数
template<class T>
class BlockQueue
{
public:
    //初始化容量和锁以及条件变量
    BlockQueue() :_cap(CAP)
    {
        //初始化锁和条件变量
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_comsumerCond,nullptr);
        pthread_cond_init(&_productorCond,nullptr);
    }

    bool isFull()
    {
        return _cap == _q.size();
    }

    bool isEmpty()
    {
        return _q.empty();
    }

    //添加
    void push(const T& in)
    {
        //添加数据的时候需要互斥访问
        pthread_mutex_lock(&_mutex);

        //因为有容量的限制,那么如果在数据已经满了的情况下,不能再添加数据了,只能等待
        while(isFull())
        {
            //等待的是自己的条件变量
            pthread_cond_wait(&_productorCond,&_mutex);

            //等待完成之后这个锁会重新回归这个线程,这部分工作由wait这个接口自己完成
            //为了防止多线程同时被唤醒的操作,所以上面的条件判断需要通过循环来二次规避
        }
        _q.push(in);

        //添加完成数据之后,可以去唤醒消费者线程消费了,因为生产者一定会放一个数据进入
        pthread_cond_signal(&_comsumerCond);

        //使用完成之后需要释放锁
        pthread_mutex_unlock(&_mutex);
    }

    //删除
    void pop(T* out)
    {
        //同理,对于拿数据也需要对临界资源做出相应的保护措施
        pthread_mutex_lock(&_mutex);

        //如果数据已经为空了,消费者应该处于一个等待状态
        while(isEmpty())
        {
            pthread_cond_wait(&_comsumerCond,&_mutex);
        }
        *out = _q.front();
        _q.pop();

        pthread_cond_signal(&_productorCond);

        pthread_mutex_unlock(&_mutex);
    }

    //释放条件变量和锁
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_comsumerCond);
        pthread_cond_destroy(&_productorCond);
    }

private:
    //共享队列,容量,锁,各自的条件变量
    queue<T> _q;
    const int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _comsumerCond;
    pthread_cond_t _productorCond;
};


        以上就是我对线程同步和生产者消费者模型的全部理解了,希望能够帮助到大家。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/31019.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数仓架构“瘦身”,Hologres 5000CU时免费试用

Hologres基于创新的HSAP架构&#xff0c;可以将您原先数仓架构中的OLAP系统&#xff08;Greenplum、Presto、Impala、ClickHouse&#xff09;、KV数据库/Serving系统&#xff08;HBase、Redis&#xff09;统一在一个大数据计算引擎中&#xff0c;并提供快速的离线实时一体化分析…

智能家居APP软件开发有何优势?

传统家居行业营销模式已经无法满足现代人多样化个性化的需求&#xff0c;也跟不上互联网时代的发展步伐了&#xff0c;很多传统家居行业都陷入了营销困境。通过智能家居APP软件开发&#xff0c;可以利用互联网改造传统模式&#xff0c;探索新的发展模式&#xff0c;可以说智能家…

近期煤矿事故及电力综合自动化系统的介绍

安科瑞虞佳豪 5月29日&#xff0c;山西灵石红杏鑫鼎泰煤业有限公司发生一起死亡1人的安全事故&#xff1b;5月24日&#xff0c;山西华阳集团新能股份有限公司二矿发生一起死亡1人的安全事故。 ​山西省应急管理厅、山西省地方煤矿安全监督管理局责令山西灵石红杏鑫鼎泰煤业有…

【设计模式与范式:总结型】74 | 总结回顾23种经典设计模式的原理、背后的思想、应用场景等

到今天为止&#xff0c;23 种经典的设计模式已经全部讲完了。咱们整个专栏也完成了 3/4&#xff0c;马上就要进入实战环节了。在进入新模块的学习之前&#xff0c;我照例带你做一下总结回顾。23 种经典设计模式共分为 3 种类型&#xff0c;分别是创建型、结构型和行为型。今天&…

经典的设计模式21——策略模式

文章目录 策略模式 猛的发现策略模式和状态模式的结构图长得到好像&#xff0c;可得好好区分区分。 不过真的好像&#xff0c;就是方法那里传递的参数不一样。 百度来一波。 策略模式 定义&#xff1a; 定义了算法家族&#xff0c;分别封装起来&#xff0c;让他们之间可以互…

智能无线监测器的工作原理及应用优势

在现代工业生产中&#xff0c;设备状态监测对于确保生产的安全性、效率和可靠性至关重要。随着科技的不断发展&#xff0c;智能无线监测器成为工业设备状态监测的利器。本文将介绍智能无线监测器在工业领域中的应用&#xff0c;以及其带来的优势和价值。 图.设备状态监测&#…

TypeScript ~ 掌握基本类型 ②

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; TypeScript ~ TS &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &…

SKD240

SKD240 系列智能电力仪表 SKD240 系列智能电力仪表是陕西斯科德智能科技有限公司自主研发、生产的。 产品概述 - 点击了解详情 SKD240采用先进的微处理器和数字信号处理技术&#xff08;内置主芯片采用32位单片机, 采用32位浮点型真有效值处理数据&#xff09;&#xff0c;测量…

南卡OE骨传导开放式蓝牙耳机评测!舒适与音质并存!

平时买耳机的时候&#xff0c;你最先会关注什么方向呢&#xff1f;是舒适、美观&#xff0c;还是音质、防水&#xff1f; 对于我来说&#xff0c;首先是功能。作为一个经常健身、跑步的人&#xff0c;最讨厌的就是平时运动流汗进入耳朵之后那种粘腻感觉。时间长了还容易让耳道…

Windows安装postgresql数据库图文教程

数据库使用排行榜&#xff1a;https://db-engines.com/en/ranking 目录 一、软件简介 二、软件下载 三、安装教程 四、启动教程 一、软件简介 PostgreSQL是一种特性非常齐全的自由软件的对象-关系型数据库管理系统&#xff08;ORDBMS&#xff09;&#xff0c;是以加州大学计…

Postman快速入门(一)

一、基本介绍 postman是一款流程的接口调试工具&#xff0c;其特点就是使用简单&#xff0c;功能强大。使用角色也非常广泛&#xff0c;后端开发&#xff0c;前端人员&#xff0c;测试人员都可以使用它进行接口调试或测试。 下图是基本功能介绍 发送第一个请求 如果你是第一次…

免费在线压缩图片的网站

1. TinyPNG - 这是一个非常受欢迎的在线图片压缩网站,可以压缩 PNG 和 JPG 图片,保证无损压缩。 网址&#xff1a;TinyPNG – Compress WebP, PNG and JPEG images intelligently 2. Compressor.io - 这也是一个很好的在线图片压缩工具,可以批量上传和压缩图片,支持 PNG, JPG 和…

安卓进阶(一)App性能优化

文章目录 性能优化的目的及方向流畅性启动速度页面显示速度响应速度 稳定性ANRCrash 资源节省性 布局优化选择耗费性能较少的布局减少布局的层级&#xff08;嵌套&#xff09;使用布局标签尽量少用布局属性wrap_contentincludemergeinclude与merge的区别ViewStub 内存泄露常见内…

前沿技术|人工智能的崛起和发展历程

前言&#xff1a; 人工智能的作用是使计算机能够模仿人类智能和学习能力&#xff0c;从而实现自动化、智能化和优化决策的目标。 文章目录 人工智能背景介绍发展状态未来展望 总结 人工智能 背景 人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;的产生…

多模态大模型综述: LLaVA, MiniGPT4

文章目录 LLaVA一. 简介1.1. 摘要1.2. 引言 二. 相关工作三. 基于GPT辅助的视觉指令数据生成四. Visual Instruction Tuning4.1 网络结构4.2 训练 LLaVA 一. 简介 题目: Visual Instruction Tuning 机构&#xff1a;微软 论文: https://arxiv.org/pdf/2304.08485.pdf 代码&am…

大数据大作业(课程设计)

题目&#xff1a;信息爬取字数统计及可视化 内容及要求&#xff1a; 配置Hadoop平台&#xff1b;利用爬虫技术爬取任一门户网站新闻栏目一定时间段内的新闻信息&#xff0c;保存为一个或多个文件并上传到Hadoop平台以本人学号命名的文件夹下&#xff1b;利用MapReduce框架编程完…

【java】JDK21 要来了

文章目录 前言更丝滑的并发编程模式虚拟线程&#xff08;Virtual Threads&#xff09;结构化并发&#xff08;Structured Concurrency&#xff09;作用域值&#xff08;Scoped Values&#xff09; 试验一下虚拟线程的例子结构化编程的例子Scoped Values 的例子 前言 不过多久&…

大数据分析案例-基于LightGBM算法构建航空公司满意度预测模型

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

Nexus搭建Maven私有库介绍

为什么需要Maven私有库&#xff1f; 使用Maven获取Java依赖包的时候&#xff0c; 默认是从Maven的中央库下载 jar文件&#xff0c; 中央库的地址是&#xff1a; https://repo.maven.apache.org/maven2 。 如果下载速度慢&#xff0c; 可以使用阿里的镜像&#xff0c; 地址如下…

【服务器数据恢复】HP LeftHand存储raid5不可用的数据恢复案例

HP LeftHand存储简介&#xff1a; HP LeftHand存储支持搭建RAID5、RAID6、RAID10磁盘阵列&#xff0c;支持卷快照&#xff0c;卷动态扩容等。服务端和客户端分别如下&#xff1a; LeftHand存储共有三个级别&#xff1a;物理磁盘、基于多个物理磁盘组成的逻辑磁盘&#xff08;ra…