Linux--多线程(2)

news2024/11/16 16:02:15

目录

    • 1. 条件变量
    • 2. 生产者消费者模型
      • 2.1 概念
    • 3. 基于BlockingQueue的生产者消费者模型
      • 3.1 概念
      • 3.2 等待函数
      • 3.3 等待函数的功能
      • 3.4 唤醒函数
    • 4. 模型复盘
    • 5. 总代码

1. 条件变量

当一个线程互斥地访问某个变量或者临界资源时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。

所以一般只有锁的情况下,我们很难知道临界资源的状态。

那么此时就引入了一种策略叫做条件变量,可以让用户知道临界资源的状态。

  • 条件变量相关函数:

在这里插入图片描述

cond:要初始化的条件变量;attr:NULL

  • 创建了条件变量,还需要在某个条件变量下等待:

在这里插入图片描述

mutex:互斥量

  • 为什么pthread_ cond_ wait 需要互斥量?

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。

条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

更通俗地说,就是需要其他线程来改变等待的条件,使得正在等待的线程不会阻塞在条件变量下。 但是这个改变的过程就是访问临界区,所以需要加锁来保证线程安全;所以需要互斥量的保护。

  • 创建和等待有了,还需要有能通知用户的方法:

在这里插入图片描述

这个signal函数的作用是唤醒在该条件变量下等待的第一个线程,由于会有多个线程等待,所以会有一个等待队列,这个队列在条件变量中,每次唤醒的是该队列里的第一个线程。

broadcast代表唤醒所有线程。

写一个程序,让一个线程控制另一个线程:

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

pthread_mutex_t mtx;//互斥锁
pthread_cond_t cond;//条件变量

//让ctrl控制work线程,让它定期运行
void* ctrl(void* args)
{
    string name = (char*)args;
    while(true){
        cout<< "master says begin work...." <<endl;
        pthread_cond_signal(&cond);
        sleep(1);
    }
}
void* work(void* args)
{
    int number = *(int*)args;
    delete (int*)args;
    while(true){
        //worker线程进来首先要等待命令
        pthread_cond_wait(&cond,&mtx);
        cout<< "worker:" << number << " is working...."<<endl;
    }
}
#define NUM 3
int main()
{
    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);

    pthread_t master;
    pthread_t worker[NUM];
    pthread_create(&master,nullptr,ctrl,(void*)"master");
    for(int i = 0;i < NUM;i++){
        int* number = new int(i);
        pthread_create(worker+i,nullptr,work,(void*)number);
    }
    //线程等待
    for(int i = 0;i < NUM;i++){
        pthread_join(worker[i],nullptr);
    }
    pthread_join(master,nullptr);

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

运行结果:

在这里插入图片描述

这也就验证了上面说的等待队列的问题;因为三个线程每次运行完以后,都会再次以这个顺序进行等待,这不是巧合,这是等待队列产生的效果。

更深层次地,可以知道条件变量应该就是一个结构体,里面不仅包含了某些数据,还包含了一个等待队列。

将signal改为broadcast以后,将一次唤醒所有进程:

pthread_cond_broadcast(&cond);

运行结果:

在这里插入图片描述

2. 生产者消费者模型

2.1 概念

在这个模型中,生产者消费者其实都是一个个的线程,由于生产者与消费者之间有非常强的耦合关系,所以需要有一个方法来使其进行解耦。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

在日常生活中,生产者可以理解为供货商,消费者可以理解为顾客,缓冲区理解为超市。

那么这个超市就是一个临界资源,需要被保护起来。

口头来说,这个模型就是“321”:3代表三种关系,分别是生产者vs生产者、生产者vs消费者、消费者vs消费者;2代表两种角色,也就是两种执行流,分别是生产者和消费者;1代表一种交易场所,也就是一段缓冲区。

3. 基于BlockingQueue的生产者消费者模型

3.1 概念

BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

  1. 当生产满了的时候,就应该不要生产了(不要竞争锁了),而应该让消费者来消费;
  2. 当消费空了,就不应该消费(不要竞争锁了),让生产者来进行生产。

首先定义阻塞队列:

class BlockQueue
{
    const int default_cap = 5;//自定义队列元素缺省值
private:
    std::queue<T> bq_;//阻塞队列
    int capacity_;     //队列的元素上限
    pthread_mutex_t mtx_;//保护临界资源的锁
    //bq_空了,就让消费者在该条件变量下等待
    //bq_满了,就让生产者在该条件下等待
    pthread_cond_t full_;//条件为满的条件变量
    pthread_cond_t empty_;//条件为空的条件变量
}

bq_是阻塞队列,用来进行模拟生产者消费者数据的消费与生产。

基本的思路就是:在有数据被生产的时候让消费者进行消费,在有数据被消费的时候让生产者进行生产。

  • 那么消费者和生产者各自怎么能知道自己什么时候该干活?

只有生产者才知道消费者什么时候该消费,也只有消费者知道生产者什么时候该生产。 可以这么理解:生产者提供了产品,它就知道消费者需要消费了,而消费者消耗了产品,它就知道生产者需要生产了,这并不是交易场所的任务,而是双方的任务。

也就是说,在有数据被生产的时候,由生产者来通知消费者过来消费;在有数据被消费了的时候,由消费者通知生产者过来生产:

//输入型参数用const T& 
void Push(const T& in)
{
	//操作临界区先上锁
    LockQueue();
    //向队列中放数据,生产函数
    
    if(isFull()){
    //如果队列满,就让生产者挂起,停止生产
        ProducterWait();
    }
    bq_.push(in);
    //走到这里肯定有数据被生产
    //虽队列不满,但是已有数据可消费,唤醒消费者
    WakeupConsumer();
    UnLockQueue();
}
//输出型参数用T*
void Pop(T* out)
{
    //向队列中拿数据,消费函数
    LockQueue();
    if(isEmpty()){
    //队列是空的,无法消费
        ConsumerWait();
    }
    *out = bq_.front();
    bq_.pop();
    //走到这里肯定消费了数据
    //那么此时适合生产,唤醒生产者
    WakeupProducter();
    UnLockQueue();
}

这是基本的思路,其中等待函数和唤醒函数的封装如下:

3.2 等待函数

等待函数的意义是在不适合进行对应操作的时候(比如队列已满,生产者不必继续生产),让该对象进行挂起等待,直到有条件继续进行操作

对于生产者来说,它需要等待的情况只有队列已满,所以说它要恢复生产,就要等队列不为满,要去full_的条件变量下等待

对于消费者来说,它需要等待的情况只有队列为空,所以说它要恢复消费,就要等队列不为空,要去empty_的条件变量下等待

void ProducterWait()
{
    pthread_cond_wait(&empty_,mtx_);
}
void ConsumerWait()
{
    pthread_cond_wait(&full_,mtx);
}

3.3 等待函数的功能

这里有个问题值得注意:线程是带着锁进入等待的,那如果它一直等不到条件满足,那别的线程岂不是无法拿到锁资源了?

所以可以让等待函数在进入前自己把锁释放

但是如果条件满足,等待函数执行完毕,执行到临界区,该线程此时未上锁,岂不是会造成线程安全问题?

所以cond_wait函数同时拥有两个功能:

  1. 调用生产者等待的时候,会首先自动释放mtx,然后再挂起自己而不是一直带着锁挂着,占有资源
  2. 返回的时候,会首先自动竞争锁,获取到锁之后才能返回

有了这两个功能,上述问题就可以解决了

首先就是不过度占有资源。 如果没有第一点,某线程在进行等待的时候会带着锁进入等待队列,那么别的进程也无法进行锁竞争。

其次就是保证线程安全。 如果没有第二点,某线程等待完毕以后将会出现在临界区,由于没有上锁,会造成数据混乱;但是如果要求该线程等待完成以后马上进行锁竞争,就不会有这种情况了。

这里有一个细节,我们进行条件检测的时候需要进行循环检测,以保证退出循环一定是由于条件不满足,因为不排除会有这些情况:

  1. 挂起失败

这个可以理解为cond_wait函数调用失败

  1. 伪唤醒

可以理解为条件没有满足,但是程序却按照满足的情况往下走了

如果仅仅使用一次 if 判断,是不太完善的,这样的情况发生以后会继续向下走,会造成后续的生产函数错误。

总之要进行消费与生产,只要是按照对应条件下来进行的,就不会有原则性的错误。

所以上述条件判断应该改为:

while(isEmpty())
while(isFull())

3.4 唤醒函数

等待的函数不是自己能知道条件已满足,继续进行操作的,而是需要有一个唤醒的过程,通过生产者唤醒消费者,消费者唤醒生产者:

void WakeupConsumer()
{
    //消费者正在empty_条件变量下等待
    pthread_cond_signal(&empty_);
}
void WakeupProducter()
{
    //生产者正在full_条件变量下等待
    pthread_cond_signal(&full_);
}

4. 模型复盘

5. 总代码

测试函数:

#include "BlockQueue.hpp"
using namespace zcb;
using namespace std;

void* consumer(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;
    while(true){
        sleep(2);
        int data = 0;
        bq->Pop(&data);
        cout<< "消费者消耗数据...->" << data << endl;
    }
}
void* producter(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;
    while(true){
        int data = rand()%20 + 1;
        cout<< "生产者生产数据...->" << data << endl;
        bq->Push(data);
    }
}
int main()
{
    //随机数种子
    srand((long long)time(nullptr));
    //交易场所
    BlockQueue<int>* bq = new BlockQueue<int>();
    pthread_t c,p;
    //bq传进去,拿到同一份交易场所
    pthread_create(&c,nullptr,consumer,(void*)bq);
    pthread_create(&p,nullptr,producter,(void*)bq);
    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    return 0;
}

头文件实现:

#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <queue>
#include <time.h>
#include <cstdlib>

#pragma once

namespace zcb
{
    template<class T>
    class BlockQueue
    {
        const int default_cap = 5;
    private:
        std::queue<T> bq_;//阻塞队列
        int capacity_;     //队列的元素上限
        pthread_mutex_t mtx_;//保护临界资源的锁
        //bq_空了,就让消费者在该条件变量下等待
        //bq_满了,就让生产者在该条件下等待
        pthread_cond_t full_;
        pthread_cond_t empty_;
    private:
        bool isFull()
        {
            return bq_.size() == capacity_;
        }
        bool isEmpty()
        {
            return bq_.size() == 0; 
        }
        void LockQueue()
        {
            pthread_mutex_lock(&mtx_);
        }
        void UnLockQueue()
        {
            pthread_mutex_unlock(&mtx_);
        }
        void WakeupConsumer()
        {
            //消费者正在empty_条件变量下等待
            pthread_cond_signal(&empty_);
        }
        void WakeupProducter()
        {
            //生产者正在full_条件变量下等待
            pthread_cond_signal(&full_);
        }
    public:
        BlockQueue(int cap = default_cap)
            :capacity_(cap)
        {
            pthread_mutex_init(&mtx_,nullptr);
            pthread_cond_init(&full_,nullptr);
            pthread_cond_init(&empty_,nullptr);
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&empty_);
            pthread_cond_destroy(&full_);
        }
        void ProducterWait()
        {
            //在是否为空的条件变量下等待才有意义

            //1. 调用生产者等待的时候,会首先自动释放mtx,然后再挂起自己
            //而不是一直带着锁挂着,占有资源
            //2. 返回的时候,会首先自动竞争锁,获取到锁之后才能返回
            pthread_cond_wait(&empty_,mtx_);
        }
        void ConsumerWait()
        {
            //在是否满的条件变量下等待才有意义
            pthread_cond_wait(&full_,mtx);
        }
    public:
        //输入型参数用const T&
        //队列使用前要加锁
        void Push(const T& in)
        {
            LockQueue();
            //向队列中放数据,生产函数
            while(isFull()){
                ProducterWait();
            }
            bq_.push(in);
            //走到这里肯定有数据被生产
            //队列不满,但是已有数据可消费,唤醒消费者
            WakeupConsumer();
            UnLockQueue();
        }
        //输出型参数用T*
        void Pop(T* out)
        {
            //向队列中拿数据,消费函数
            LockQueue();
            while(isEmpty()){
                //无法消费
                ConsumerWait();
            }
            *out = bq_.front();
            bq_.pop();
            //走到这里肯定消费了数据
            //那么此时适合生产,唤醒生产者
            WakeupProducter();
            UnLockQueue();
        }
    };
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

物以类聚人以群分,通过GensimLda文本聚类构建人工智能个性化推荐系统(Python3.10)

众所周知&#xff0c;个性化推荐系统能够根据用户的兴趣、偏好等信息向用户推荐相关内容&#xff0c;使得用户更感兴趣&#xff0c;从而提升用户体验&#xff0c;提高用户粘度&#xff0c;之前我们曾经使用协同过滤算法构建过个性化推荐系统&#xff0c;但基于显式反馈的算法就…

【Java寒假打卡】Java基础-集合Map

【Java寒假打卡】Java基础-集合Map基本使用Map集合的基本功能Map集合的第一种遍历方式Map集合的第二种遍历方式案例&#xff1a;HashMap集合存储ArrayList元素并遍历案例&#xff1a;统计字符串中每一个字符出现的次数Collections操纵集合基本使用 创建Map集合的对象&#xff1…

金融历史数据导入之股票 level2 逐笔篇

在部署完 DolphinDB 后&#xff0c;将历史数据导入数据库是后续进行数据查询、计算和分析的基础。为协助用户快速导入数据&#xff0c;本文档基于 DolphinDB 已有的教程与大量用户的实践经验&#xff0c;从操作者角度出发&#xff0c;以 CSV 格式的文件为例&#xff0c;详细介绍…

通讯电平转换电路中的经典设计

今天给大家分享几个通讯电平转换电路。 有初学者问&#xff1a;什么是电平转换&#xff1f;举个例子&#xff0c;比如下面这个电路&#xff1a; 单片机的工作电压是5V&#xff0c;蓝牙模块的工作电压是3.3V&#xff0c;两者之间要进行通讯&#xff0c;TXD和RXD引脚就要进行连接…

Revit里轴网隐藏尺寸标注跟着消失?快速轴网距离标注

一、Revit中链接项目文件轴网的巧妙处理 问题&#xff1a;在单元式住宅体系中&#xff0c;轴网的使用主要是对尺寸标注的影响&#xff0c;如果要将子文件链接到父文件中&#xff0c;需要隐藏轴网&#xff0c;这样与轴网关联的尺寸标注就会消失。 关于尺寸标注与轴网隐藏方式的关…

Java IO流(基础详解,快速上手!)

文章目录概述什么是IO流&#xff1f;常用的文件操作获取文件操作目录操作IO流的原理和分类概述 在Java的学习中&#xff0c;文件和IO流是一个十分重要的板块。在Java中&#xff0c;File是文件和目录路径名的抽象表示。文件和目录可以通过File封装成对象。对File而言&#xff0…

前端 base64与图片相互转换

base64转图片 如下图&#xff1a;&#xff08;后端返回的数据&#xff09; <img :src"baseImg" >let baseImg "" this.baseImg "data:image/png;base64," data?.flowCharbase64转换图片文件 base64ImgtoFile (dataurl, filename …

QT 学习笔记(十六)

文章目录一、TCP 传文件流程图1. 服务器端流程2. 客户端流程二、TCP 传文件操作实现1. 服务器端2. 客户端3. TCP 传文件实现现象三、服务器端和客户端实现代码1. 主函数 main.c2. 服务器端头文件 serverwidget.h3. 服务器端源文件 serverwidget.cpp4. 客户端头文件 clientwidge…

某医院的实战渗透测试(组合拳)

实战渗透一、前言二、Spring信息泄露三、Redis写公钥四、文章来源一、前言 项目是内网环境下进行&#xff0c;所以通过vpn接入内网之后进行目标系统的测试。&#xff08;信息泄露redis写公钥&#xff09; 二、Spring信息泄露 访问客户给的目标地址通过代理把流量转给了BurpS…

零基础学软件测试有前途吗?

随着软件工程活动的不断演化&#xff0c;测试工作某种程度上是可以很大幅度提高软件的产品质量以及提升用户的使用满意度&#xff0c;因此软件测试工程师的地位在企业中也越来越受到重视。不少零基础学IT的朋友也开始把软件测试作为一个绝佳的选择对象&#xff0c;那么零基础学…

leetcode.1806 还原排列的最少操作步数 - 模拟 + lcm

​​​​​​1806. 还原排列的最少操作步数 本题是数论题 共介绍4种解题方法 目录 1、所有置换环长度的最小公倍数 2、最小操作数是最大环长度 3、1或n-2所在环长度即为最大置换环长度 4、暴力模拟 思路&#xff1a; 因为数据范围很小 所以可以直接模拟 也可以优化一下—…

Python 模型训练:LSTM 时间序列销售额预测(训练、保存、调用)

LSTM (long short-term memory) 长短期记忆网络&#xff0c;具体理论的就不一一叙述&#xff0c;直接开始 流程一、数据导入二、数据归一化三、划分训练集、测试集四、划分标签和属性五、转换成 LSTM 输入格式六、设计 LSTM 模型6.1 直接建模6.2 找最好七、测试与图形化展示八、…

JavaSE-07

字节流输入输出数据&#xff1a; InputStream和OutputStream作为字节流输入输出流的超类。 字节流写数据时千万记得close关闭资源&#xff0c;可设置追加写为true 字节流读数据时&#xff0c;FileInputStream a new FileInputStream (“”); int by a.read(); char b (char…

隐蔽信道学习

隐蔽信道作为一种能够在不被系统感知的情况下稳定窃取秘密信息的通信手段&#xff0c;尽管其带宽通常较低&#xff0c;但其设计上的复杂性和多样性&#xff0c;使得常规的流量审计系统难以对抗或检测。同时&#xff0c;隐蔽信道也是密钥、身份认证、商业机密等秘密信息传输的重…

基于JAVA SSM框架的新闻管理系统源码+数据库,实现 登录 、 注册、 新闻内容、类别、评论、个人信息、系统管理等功能

[基于SSM框架的新闻管理系统] 前言 下载地址&#xff1a;基于JAVA SSM框架的新闻管理系统源码数据库 基于SSM框架的新闻管理系统&#xff1b; 实现 登录 、 注册 、 新闻内容、类别、评论、个人信息、系统管理等功能 &#xff1b; 可继续完善增加前端、等其他功能等&#x…

federated引擎实现mysql跨服务器表连接

&#x1f4e2;作者&#xff1a; 小小明-代码实体 &#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/as604049322 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 欢迎讨论&#xff01; &#x1f4e2;本文链接&#xff1a;https://xxmdmst.blog.csdn.n…

IU5066 高耐压带OVP保护1.2A单节锂电池线性充电IC

概要 IU5066E是面向空间受限便携应用的&#xff0c;高度集成锤离子和锤聚合物线性充电器器件。该器件由USB端口或交流适配器供电。带输入过压保护的高输入电压范围支持低成本、非稳压适配器。 电池充电经历以下三个阶段&#xff1a; 涓流、电流、恒压。在所有充电阶段&#x…

JSON对象(javascript)

本文内容主要包括了对于JS中JSON对象的一些内容。我们知道JSON格式是前后端进行信息交换的中介信息格式。适用于取代XML格式的一种格式&#xff0c;在多数编程语言中都有关于JSON的处理方法。那么javascript也提供了JSON对象用于处理相应的数据。 1. 什么是JSON格式&#xff1…

mac安装jdkidea配置jdk

第一步&#xff1a;mac安装jdk1、下载jdk&#xff0c;下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads/#java82、安装后&#xff0c;终端输入java -version查看java是否安装成功3、配置环境变量a.在终端输入 /usr/libexec/java_home 可以得到JAVA_HOM…

【矩阵论】5. 线性空间与线性变换——线性映射与自然基分解,线性变换

矩阵论 1. 准备知识——复数域上矩阵,Hermite变换) 1.准备知识——复数域上的内积域正交阵 1.准备知识——Hermite阵&#xff0c;二次型&#xff0c;矩阵合同&#xff0c;正定阵&#xff0c;幂0阵&#xff0c;幂等阵&#xff0c;矩阵的秩 2. 矩阵分解——SVD准备知识——奇异值…