线程 --- 同步与生产消费者模型

news2024/11/15 8:44:38

序言

 在上一篇的内容中,我们学习了使用互斥锁来保护共享资源,避免多个线程竞争,造成数据不一致等问题。在这一篇文章中,我们将继续深入,学习多线程同步以及生产消费者模型。


1. 线程同步

1.1 什么是线程同步

 线程互斥只是解决了多个线程之间资源竞争的问题,但他不能保证线程之间的 执行顺序以及任务合理分配等问题。就比如下段代码,具体的逻辑是多个线程进行抢票,记录每一个线程抢的票数(线程执行次数):

// 指定抢票的数量
int Tickets = 10000;
// 全局的锁,保证对共享资源安全的访问
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;

......
    static void *func_t(void *arg)
    {
        MyThread *Ptr = static_cast<MyThread *>(arg);
        while (true)
        {
            pthread_mutex_lock(&gmutex);
            if (Tickets > 0)
            {
                Tickets--;
                std::cout << Ptr->getName() << " get a tickets there left " << Tickets << std::endl;
                pthread_mutex_unlock(&gmutex);
                Ptr->CountIncrease();
            }
            else
            {
                pthread_mutex_unlock(&gmutex);
                break;
            }
        }

        return nullptr;
    }
......

在这里只是展示了核心的抢票代码部分,现在我们观察程序的输出结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
 我们一共运行了三次,这三次运行结果充分的向我们展示了什么是累的累死,闲的闲死。因为某些线程抢夺锁的能力会更强一些,所以说执行的次数也就越多,这就会造成 负载均衡不均,资源利用率低等问题

 为了解决这个问题,我们就需要引进一个新的概念 线程同步

 线程同步是指在多线程环境中,协调线程之间的执行顺序和数据访问,以确保它们以一种一致的方式运行。这通常涉及到确保多个线程在执行某些操作时能够按照预期的顺序进行,从而避免出现竞态条件(race condition)。线程同步的主要目的是为了保证 程序的一致性和正确性
 和线程互斥比起来,线程同步还需要兼顾多个线程之间的执行顺序。


1.2 线程同步 — 条件变量

条件变量是实现线程同步的重要工具之一,特别适用于 当线程需要在某种条件下等待或被唤醒的情况。条件变量一般与互斥锁配合使用,以保护共享数据和同步线程的执行。

1.1 概念

 条件变量允许线程在满足某些条件之前被阻塞(等待),并在条件满足时被唤醒。它主要用于以下场景:

  • 生产者-消费者问题:生产者线程在数据缓冲区满时等待,消费者线程在数据缓冲区空时等待。
  • 线程池:工作线程在任务队列为空时等待,新任务到来时唤醒线程。

1.2 条件变量的初始化

 在使用条件变量之前,必须初始化互斥锁和条件变量(可以使用全局也可使用局部的):

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
// 或者使用 pthread_mutex_init 和 pthread_cond_init 函数进行初始化。

1.3 锁定互斥锁

 在使用条件变量之前,线程必须先锁定互斥锁

pthread_mutex_lock(&mtx);

1.4 等待条件变量

 当线程需要等待某个条件时,它 释放互斥锁并挂起自己(释放锁,避免发生死锁问题) ,等待条件变量的信号

// 进程会阻塞在此处,等待信号
pthread_cond_wait(&cv, &mtx);

1.5 发出信号

 当线程修改了共享资源,使得等待的线程可以继续执行时,发出信号通知一个或所有等待的线程:

pthread_cond_signal(&cv); // 唤醒一个等待的线程
// 或
pthread_cond_broadcast(&cv); // 唤醒所有等待的线程

1.6 解锁互斥锁

 在修改共享资源后,线程必须释放互斥锁,以便其他线程可以访问:

pthread_mutex_unlock(&mtx);

1.7 销毁

 在程序结束时,需要销毁条件变量和互斥锁:

pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cv);

1.3 实现线程同步

 在了解了体了条件变量的基本使用后,我们对我们上述的代码进行改进,保证每个线程之间顺序执行:

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void *Print(void *arg)
{
    std::string name = static_cast<const char *>(arg);
    while (true)
    {
        pthread_mutex_lock(&gmutex);

        pthread_cond_wait(&gcond, &gmutex);
        std::cout << "I am " << name << std::endl;

        pthread_mutex_unlock(&gmutex);
    }
}

const int num = 5;

int main()
{
    pthread_t threads[num];
    for (int i = 0; i < num; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread_%d", i + 1);
        pthread_create(threads + i, nullptr, Print, (void*)name);
    }

    while (true)
    {
        pthread_cond_signal(&gcond);
        sleep(1);
    }

    for (int i = 0; i < num; i++)
    {
        pthread_join(threads[i], nullptr);
    }

    return 0;
}

在这段程序中就能有序的输出每个线程的结果,避免了一家独大的情况:
在这里插入图片描述


2. 生产消费者模型

生产消费者模型(Producer-Consumer Model)是一种广泛应用于多线程编程中的设计模式,旨在通过引入一个缓冲区(或称为队列)来 解耦生产数据和消费数据的过程,实现生产者和消费者之间的 平衡和协作

2.1 概念

 该模型由三个对象所构成,分别是 生产者,缓冲区,消费者:

  • 生产者(Producer):负责生成数据并将其放入缓冲区。生产者可以是一个或多个线程/进程,它们不断地生成数据以供消费者处理。
  • 消费者(Consumer):负责从缓冲区中取出数据进行处理。消费者同样可以是一个或多个线程/进程,它们不断从缓冲区中获取数据并执行相应的处理操作。
  • 缓冲区(Buffer/Queue):作为生产者和消费者之间的中介,用于存储生产者生成的数据,供消费者取用。缓冲区的大小通常是有限的,以避免无限制地存储数据导致内存溢出。

在这里我们引入实际生活中的例子来帮助大家理解:
在这里插入图片描述
我们在生活中经常前往超市购物吧,此时工厂就充当生产者,超市就充当缓冲区,我们就充当消费者。当我们需要相应的商品时,会直接前往商店购物而不是工厂,商场会根据当前库存的量告诉工厂,最近消费多我需要多进货或者是最近消费低我进货速度稍微慢一点,这样就很好的调节了生产与消费相适应的速度。

2.2 特点

 我们依旧使用 工厂 - 超市 - 消费者 来阐述该系统的特点:

  • 并发性:生产者和消费者可以并发执行,提高了程序的效率和响应速度。
  • 负载均衡:货物摆放策略:超市可能会优化货架的摆放策略,以平衡顾客的购物需求和供应商的补货速度。
  • 解耦合:货物补充和购物:顾客可以随时购物,而供应商则会在不同的时间点进行补货,顾客购物的过程不会阻碍供应商的补货工作。

2.3 工作原理

生产者流程
  1. 生产者生成数据。
  2. 检查缓冲区是否已满。
  3. 如果缓冲区未满,将数据放入缓冲区;如果缓冲区已满,则生产者可能需要等待。
  4. 通知等待的消费者缓冲区中有新数据可取。
消费者流程
  1. 消费者检查缓冲区是否为空。
  2. 如果缓冲区不为空,从缓冲区中取出数据进行处理;如果缓冲区为空,则消费者可能需要等待。
  3. 处理完数据后,根据需要通知生产者或其他消费者。

2.4 代码实现

 在这里我们使用队列作为我们的缓冲区,在开始之前,现提出几个问题:

  • 若缓冲区已满,则生产者不再生产,需要阻塞等待,那么谁唤醒他呢?
  • 若缓冲区为空,则消费者不能获取,需要阻塞等待,这又谁唤醒他呢?

 又从我们的商店视角来看,当货架满了,肯定就不能进货了,工厂停止生产,那什么时候告诉工厂可以生产呢?消费者获取数据时!消费者获取数据了则货架肯定不是满的,商店就可以告诉工厂你们可以生产啦!反之一样的,当货架为空,什么时候可以获取数据呢?工厂生产时!工厂生产了,则肯定上货架了,消费者就可以获取数据了。
 费者和生产者通常 通过互相唤醒来协调操作:

  • 生产者:在生产数据后可能会通知或唤醒消费者,特别是当缓冲区有新的数据时。
  • 消费者:在处理完数据后,可能会通知或唤醒生产者,特别是当缓冲区有足够空间时。

这种互相唤醒机制 有助于保持系统的平衡,避免阻塞和空闲状态,从而提高整体效率。

构造函数和析构函数
template <class T>
class BlockQueue
{
    BlockQueue(int Cap = DefalutCapacity)
        : _Cap(Cap);
    {
        // 初始化锁
        pthread_mutext_init(&_mutex, nullptr);
        // 初始化条件变量
        pthread_cond_init(&_p_cond, &_mutex);
        pthread_cond_init(&_c_cond, &_mutex);
    }

    ~BlockQueue()
    {
        // 删除锁
        pthread_mutex_destroy(&_mutex);
        // 删除条件变量
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }

private:
    std::queue<T> _BlockQueue; // 缓冲区存储临界资源
    int _Cap;                  // 缓冲区容量
    pthread_mutex_t _Mutext;   // 锁
    pthread_cond_t _p_cond;    // 生产者的条件变量
    pthread_cond_t _c_cond;    // 消费者的条件变量
};
  • 在这里使用互斥锁 保证在多个线程同时访问时,队列的状态(如队列的大小、队列中的元素等)保持一致性。
  • 准备两个条件变量,分别在特殊情况时让消费者和生产者阻塞等待

生产接口

 所谓生产接口就是往队列中写入数据:

    void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        if(IsFull())
        {
            pthread_cond_wait(&_p_cond, &_mutex);
        }

        _BlockQueue.push(in);
        pthread_mutex_unlock(&_mutex);
        // 通知消费者可以消费了
        pthread_cond_signal(&_c_cond);
    }
  • 对临界区进行访问,首先需要上锁
  • 判断缓冲区(队列)是否已满
  • 若已满则阻塞等待,反之写入数据
  • 解锁,并且通知消费者消费

BUG警告:在这里乍一看是没什么问题的,但是我们需要思考特殊情况,加入现在有两个生产者 A,B 被阻塞等待,当一个消费者取出数据后,向生产者发出生产信号,其中 A 接受信号并且抢夺了锁往后执行,现在 A 写入数据,队列再次为满。A 执行完成后,释放了锁并且通知消费者消费,但是请注意 B 在之前已经被被释放了,所以也会参与抢锁的过程,如果锁被 B 抢到了,那是不是就会有问题了?明明队列为满,生产者还是往里写数据,这是不是就会出错了 ! 这就是 虚假唤醒,线程在没有满足预期条件的情况下被唤醒,被唤醒的线程可能继续执行后续操作,而这些操作可能基于一个已经不再满足的条件。

 解决方案也非常简单,因为 if 只能判断一次,所以我们换为 while 轮询的检查是否满足生产的条件:

   while(IsFull())
   {
       pthread_cond_wait(&_p_cond, &_mutex);
   }

消费接口

 消费接口就是消费者读取数据的接口:

    // 消费接口
    void pop(T &out)
    {
        pthread_mutex_lock(&_mutex);
        while (IsEmpty())
        {
            pthread_cond_wait(&_c_cond, &_mutex);
        }

        out = _BlockQueue.front();
        _BlockQueue.pop();

        pthread_mutex_unlock(&_mutex);
        // 通知生产者可以生产了
        pthread_cond_signal(&_p_cond);
    }

逻辑和生产接口是相差不多的,在这里就不过多解释了哦。

3. 总结

 在本文中我们介绍了线程同步的概念,以及基于线程同步的生产消费者模型,还是尝试实现了该模型的核心代码,希望大家有所收获!

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

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

相关文章

操作系统常见面试题总结

文章目录 1 操作系统基础1.1 什么是操作系统&#xff1f;1.2 操作系统主要有哪些功能&#xff1f;1.3 用户态和内核态1.3.1 什么是用户态和内核态&#xff1f;1.3.2 为什么要有用户态和内核态&#xff1f;只有一个内核态不行么&#xff1f;1.3.3 用户态和内核态是如何切换的&am…

HICOOL 2024全球创业大赛角出200个获奖项目

近日&#xff0c;经过超过200天的紧张竞争&#xff0c;HICOOL 2024全球创业大赛从124个国家和地区的7406个创新项目中脱颖而出&#xff0c;共选出了200个获奖项目。 其中&#xff0c;“新一代智能光学显微成像仪器与全流程解决方案”和“基于多尺度深度学习的功能型生物分子设…

Excel下拉框多选

记录一下学会一个新的知识&#xff01; 两种方式 第一种方式&#xff1a;先在表格里写好需要的值&#xff0c;再在数据关联里面直接引入。 1.新建excel表格&#xff0c;输入下拉框需要的值。 2.点击——数据>有效性 3.选择——序列 4.数据来源——框住刚才写好的数据——…

IMU用于动物行为监测

近日&#xff0c;由比利时和法国组成的科研团队开展了一项创行性的研究&#xff0c;通过在牛颈部安装IMU&#xff08;惯性测量单元&#xff09;&#xff0c;实现了对牛吃草行为的实时监测。该技术通过捕捉牛咀嚼时的微小动作&#xff0c;并结合机器学习算法&#xff0c;智能区分…

07--kubernetes.deploy与service

前言&#xff1a;这一章主要是deploy与service的配置及其原理&#xff0c;以及一些细节上的补充&#xff0c;同时还会附上镜像拉取策略和容器健康检查的一些操作实例&#xff0c;内容比较多&#xff0c;建议根据目录查看。 1、镜像拉取策略 IfNotPresent:在镜像已经存在的情况…

新迪天工®看图,专业的三维CAD看图工具

替代专业CAD软件&#xff0c;方便查看各种三维和二维CAD图纸 新迪天工看图是一款功能强大的三维模型和二维图纸查看工具&#xff0c;能帮助制造企业以较低的成本、较高的数据安全性实现产品设计数据的跨业务浏览和交互。 应用场景 1、设计图纸评审 可直接对三维模型和二维…

【MATLAB源码-第255期】基于matlab的长鼻浣熊优化算法(COA)无人机三维路径规划,输出做短路径图和适应度曲线.

操作环境&#xff1a; MATLAB 2022a 1、算法描述 长鼻浣熊优化算法&#xff08;Coati Optimization Algorithm&#xff0c;COA&#xff09;是一种新兴的群体智能优化算法&#xff0c;其灵感来源于长鼻浣熊在自然界中的觅食行为。长鼻浣熊是一种生活在美洲热带和亚热带森林中…

Redis中的数据类型及应用场景(面试版)

五种常用数据类型介绍 Redis中存储的都是key-value对结构的数据&#xff0c;其中key都是字符串类型&#xff0c;value有5种常用的数据类型&#xff1a; 字符串 string 哈希 hash 列表 list 集合 set 有序集合 sorted set / zset 各种数据类型特点 解释说明&#xff1a; …

DP10RF001支持200MHz~960MHz(G)FSK/OOK调制无线抄表工业传感无线遥控

简介 DP10RF001是一款工作于 200MHz~960MHz 范围内的低功耗、高性能、单片集成的(G)FSK/OOK无线收发芯片。内部集成完整的射频接收机、射频发射机、频率综合器、调制解调器&#xff0c;只需配备简单、低成本的外围器件就可以获得良好的收发性能。 芯片支持灵活可设的数据包格式…

6U VPX总线架构:搭载飞腾D2000/FT2000 + FPGA-K7(赛灵思)

"CPU FPGA" 结构是指一种结合了中央处理器&#xff08;CPU&#xff09;和现场可编程门阵列&#xff08;FPGA&#xff09;的系统架构。这种架构利用了CPU的通用计算能力和FPGA的并行处理能力&#xff0c;可以提供高效能、低延迟和高灵活性的计算平台。 K7是 Xilinx 7…

Linux进程信号——信号的捕捉、保存、处理

文章目录 信号的基本概念信号保存block位图pending位图handler数组 信号处理sigset_tsigemptysetsigfillsetsigaddset sigdelsetsigismembersigprocmask 捕捉信号 信号的基本概念 信号递达&#xff1a;实际处理信号的动作信号未决&#xff1a;信号从产生到递达之间的状态信号阻…

比人还毒舌的AI上线了!

前段时间社交媒体上很火的毒舌版AI&#xff0c;上线后异常火爆&#xff0c;网友把各路名人的账号输入&#xff0c;川普&#xff0c;马斯克等一干名人被吐槽得体无完肤。 现在这个功能国内也有了&#xff0c;推出了微博版的AI嘴替&#xff0c;微博上的名人纷纷中招。 体验地址&a…

如何修改注解里面的属性值

说明&#xff1a;Java中&#xff0c;注解里的属性值在编译时就已经固定了&#xff0c;是无法通过AOP或者反射技术直接去修改的。本文介绍如何通过动态代理的方式来修改属性值。 搭建环境 首先&#xff0c;创建一个简单的Spring Boot项目&#xff0c;pom.xml文件如下&#xff…

声音事件检测DESED 数据集介绍

DESED dataset contains:DESED Domestic Environment sound event detection; 家庭环境声音事件检测&#xff1b; 1. 数据 Content内容 DESED dataset contains:DESED 数据集包含&#xff1a; Domestic Environment sound event detection; 家庭环境声音事件检测&#xff1…

[学习笔记]在不同项目中切换Node.js版本

文章目录 使用 Node Version Manager (NVM)安装 NVM使用 NVM 安装和切换 Node.js 版本为项目指定 Node.js 版本 使用环境变量指定 Node.js安装多个版本的 Node.js设置环境变量验证配置使用 npm 脚本切换 在开发中&#xff0c;可能会遇到不同的Vue项目需要不同的Node.js&#xf…

各个版本jdk新特性

jdk8新特性 方法引用&#xff1a;方法引用允许直接通过方法的名称来引用已经存在的方法&#xff0c;简化了函数式接口的实现。默认方法&#xff08;Default Methods&#xff09;&#xff1a;默认方法允许在接口中定义具有默认实现的方法&#xff0c;以便接口的实现类可以继承该…

uniapp-Vue项目如何实现国际化,实现多语言切换,拒绝多套开发,一步到位,看这篇就够

一 安装 找到自己的项目,输入cmd进入命令行,输入安装命令,点击回车进行下载: npm install vue-i18nnext 下载完将在项目的配置文件中看到: 二 使用 2.1 在项目中创建一个文件夹如:lang 用于存放不同语言的包。这些语言文件通常为JSON格式 2.2 在项目main.js文件中引入并初…

YoloV8损失函数篇(代码加理论)

首先yolov8中loss的权重可以在ultralytics/cfg/default.yaml修改 损失函数定义ultralytics/utils/loss.py 回归分支的损失函数 DFL(Distribution Focal Loss)&#xff0c;计算anchor point的中心点到左上角和右下角的偏移量IoU Loss&#xff0c;定位损失&#xff0c;采用CIoU…

开源网络安全大模型 - SecGPT

网络安全大模型是指使用大量数据和参数来训练的人工智能模型&#xff0c;它可以理解和生成与网络安全相关的内容&#xff0c;例如漏洞报告、利用代码、攻击场景等。 目前各家网络安全厂商也纷纷跟进在大模型方面的探索&#xff0c;但可供广大从业者研究的特有网络安全大模型…

2013-2023年 中国MOD17A3H植被净初级生产力(NPP)数据

中国MOD17A3H植被净初级生产力&#xff08;NPP&#xff09;数据是基于NASA的MODIS卫星遥感数据计算得出的&#xff0c;这些数据对于评估生态系统碳收支、碳循环以及气候变化的影响具有重要意义。NPP数据可以反映植被通过光合作用固定大气中二氧化碳并转化为有机物质的能力&…