【Linux】线程互斥与同步,生产消费模型(超详解)

目录

线程互斥

进程线程间的互斥相关背景概念

数据不一致问题

深度理解锁

原理角度理解:

实现角度理解:

线程同步

条件变量

测试代码

生产消费模型

生产消费模型概念

编写生产消费模型

BlockingQueue

(1)创建生产者,消费者

(2)新线程的执行函数

(3)BlockQueue的封装


线程互斥

进程线程间的互斥相关背景概念

  • 临界资源: 多线程执行流共享的资源就叫做临界资源
  • 临界区: 每个线程内部, 访问临界资源的代码, 就叫做临界区
  • 互斥: 任何时刻, 互斥保证有且只有一个执行流进入临界区, 访问临界资源,通常对临界资源起保护作用
  • 原子性(后面讨论如何实现) : 不会被任何调度机制打断的操作, 该操作只有两态, 要么完成, 要么未完成

数据不一致问题

多线程访问一个资源时,并发访问会出现数据不一致问题;

以抢票为例:

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdlib>
using namespace std;
int num = 1000;
void *routine(void *args)
{
    while (true)
    {
        if (num > 0)
        {
            cout << (char *)args << " get a ticket :" << num << endl;
            num--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t tid[5];
    char *name[5];
    for (int i = 0; i < 5; i++)
    {
        name[i] = new char[128];
        snprintf(name[i], 128, "thread-%d", i + 1);
        pthread_create(&tid[i], nullptr, routine, (void *)name[i]);
    }
    sleep(1);
    for (int i = 0; i < 5; i++)
    {
        pthread_join(tid[i], nullptr);
        cout << "thread-" << i + 1 << "join sucess ..." << endl;
        delete[] name[i];
    }
}

运行会发现: 只有1000个票,抢的票数却大于1000;

出现上述结果的原因:

当票只有一张的时候,线程1进入了,判断了大于0了,但是正准备去抢票的时候,线程1 被切换了;此时线程2被唤醒了,因为线程1还没有来到及对num--,所以线程2也进入了,也在正准备抢票的时候被切换了;此时又进入了线程3,线程3和线程1,2一样,也是在num--之前被切换;此时只有一张票,但是却有三个线程;线程1 被唤醒,num--,票数变成了0;线程2又被唤醒,num--;票数为-1;线程3被唤醒;num--,票数为-2;

--:不是一步能做成了,而是:1、重读数据  2、--数据  3、写回数据;

那怎么解决上述的出现的数据不一致问题呢?

加锁

pthread_mutex_t:互斥锁类型

锁的定义:

加锁和解锁:

运行结果: 通过观察运行结果,确实不会出现0,-1,等问题了;

结论:

  1. 加锁的范围,粒度一定要尽量小;
  2. 任何线程,要进行抢票,都必须先申请锁;
  3. 所以线程申请锁,前提是所以线程都看得到这把锁,锁本身也是共享资源;--加锁的过程必须是原子的
  4. 原子性:要么不做,要么做完,没有中间状态;
  5. 如果线程申请锁失败了,我的线程就要被阻塞;
  6. 如果线程申请锁成功了,继续向后运行;
  7. 如果线程申请锁成功了,执行临界区代码,执行临界区代码期间,可以切换,但是其他线程是进不来的,因为虽然被切换了,但是并没有释放锁;
  8. 对于其他线程,要么我没有申请锁,要么我释放了锁,对其他线程才有意义!!!

深度理解锁

原理角度理解:

实现角度理解:

为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 

线程a申请到锁:1、寄存器al清零      2、交互寄存器al和锁中的值    3、判断(如果寄存器al中>0,说明申请到了锁,不满足条件,则没有申请到锁);

线程同步

我们以下面的例子来引入:

在一个自习室中,一次只允许一个人进入,如果一个人拿着钥匙一直进入的话就不合理了;所以管理员提出了规则:1、自习完的同学,规划钥匙后,不能立马申请;2、第二次申请,必须排队;

这样所有人访问自习室的过程,是安全的且具有一定的顺序性!---->同步---->严格的顺序性;

所以同步就是为了保护我们的顺序性;

看我们互斥时的代码允许结果,我们会发现最后都是一个线程抢到票,这样对其他线程就不公平,引入同步就能很好的解决这个问题;

条件变量

  1. 快速认识接口
  2. 认识条件变量

声明条件变量:

进行等待:

可以理解为自习室的人排队等待,在哪里等呢?在指定的条件变量下去等待;

被调用时,除了自己排队等待,还会自己释放传入的锁;

返回时,必须先参与锁的竞争,重新加上锁后,该函数才会释放; 

唤醒:

唤醒一个线程:

唤醒全部线程:

测试代码

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
const int num = 5;
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *Wait(void *args)
{
    string name = static_cast<char *>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//在条件变量下进行等待,这里就是进程等待的位置
        cout << "I am " << name << endl;
        pthread_mutex_unlock(&mutex);
        // usleep(10000);
    }
}

int main()
{
    pthread_t tid[num];
    for (int i = 0; i < num; i++)
    {
        char *buff = new char[1024];
        snprintf(buff, 1024, "thread-%d", i + 1);

        pthread_create(&tid[i], nullptr, Wait, (void *)buff);
        sleep(1);
    }

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

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

运行结果:

生产消费模型

生产消费模型概念

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

"321"原则:

  • 1:一个交易场所(特定数据结构形式存在的一段内存空间)
  • 2:两种角色(生产角色,消费角色),生成线程,消费线程
  • 3:三种关系(生产和生产,消费和消费,生成和消费) 

生产消费模型本质就是:通过代码,实现“321”原则,用锁和条件变量(或其他方式)来实现三种关系!!!

编写生产消费模型

BlockingQueue

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

(1)创建生产者,消费者

创建生产者和消费者即创建线程:pthread_create

(2)新线程的执行函数

(3)BlockQueue的封装
#pragma once

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <sys/types.h>
using namespace std;

const int defaultcap = 5;

template <typename T>
class BlockQueue
{
private:
    bool isfull()
    {
        if (_block_queue.size() == _max_cp)
            return true;
        return false;
    }
    bool isEmpty()
    {
        return _block_queue.empty();
    }

public:
    BlockQueue(int cap = defaultcap) : _max_cp(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }
    void Pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        while (isEmpty())
        {
            // 空了
            pthread_cond_wait(&_c_cond, &_mutex);
        }
        *out = _block_queue.front();
        _block_queue.pop();
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_p_cond);
    }

    void Equeue(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        while (isfull())
        {
            // 满了,生产者不能生产,必须等待
            // pthread_cond_wait被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁
            // 被唤醒的时候,会重新排队加锁
            pthread_cond_wait(&_p_cond, &_mutex);
        }
        _block_queue.push(in);
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_c_cond);
    }

private:
    queue<T> _block_queue; // 临界资源
    int _max_cp;
    pthread_mutex_t _mutex;
    pthread_cond_t _p_cond;
    pthread_cond_t _c_cond;
};

以上就是线程互斥与同步,生产消费模型的全部内容,希望有所帮助!!!

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

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

相关文章

双十一宠物空气净化器哪款吸毛好而且噪音低?希喂、IAM、有哈真实测评

家人们谁懂啊&#xff0c;家里怎么会有一只这么爱掉毛的小猫咪啊&#xff0c;看着香香软软的&#xff0c;谁知道掉起毛来六亲不认啊&#xff0c;搞得我这个老母亲筋疲力尽啊&#xff0c;每天只想着清理它掉下来的浮毛&#xff0c;主要是还特别难清理。 所以后面入手了能吸毛的…

OpenAI o1复现:自动构造prm训练数据-OmegaPRM

作者&#xff1a;cmathx 原文&#xff1a;https://zhuanlan.zhihu.com/p/1477078851 openai o1复现中&#xff0c;有个比较关键的问题&#xff0c;怎么样自动化构造prm模型的训练数据&#xff1f;本文主要从代码层面&#xff0c;来解析OmegaPRM原理。 论文 Improve Mathemat…

Discuz | 起尔开发 传奇开服表游戏公益服发布论坛网站插件

Discuz | 起尔开发 传奇开服表游戏公益服发布论坛网站插件 插件下载&#xff1a;源码 - 起尔开发的插件下载 演示地址&#xff1a;discuz.72jz.com 标黄和非标黄自动分开 在标黄时间内显示在上面置顶&#xff0c;标黄过期后自动显示在下面白色区域。 后台可以设置非标黄默认…

四、多线程带来的的⻛险-线程安全

4.1 观察线程不安全 运行以下代码&#xff1a; package demo02;public class Test {private static int count 0;public static void main(String[] args) throws Exception {Thread t1 new Thread(() -> {for (int i 0; i < 50_000; i) {count;}});Thread t2 new …

通过Docker Compose构建自己的Java项目

通过Docker Compose构建自己的Java项目 前置条件 安装了Docker,未安装的请移步:CentOS7 / CentOS8 安装 Docker-ce安装了Docker-Compose,未安装的请移步:在CentOS7、CentOS8系统下安装Docker Compose1. 配置阿里云镜像仓库 为了提高Docker镜像的下载速度,我们可以配置阿…

版本工具报错:Error Unity Version Control

NotConfiguredClientException: Unity VCS client is not correctly configured for the current user:Client config file.

python 爬虫 入门 三、登录以及代理。

目录 一、登录 &#xff08;一&#xff09;、登录4399 1.直接使用Cookie 2.使用账号密码进行登录 可选观看内容&#xff0c;使用python对密码进行加密&#xff08;无结果代码&#xff0c;只有过程分析&#xff09; 二、代理 免费代理 后续&#xff1a;协程&#xff0c;…

TitanIDE:解锁编程教学新范式

在高校软件工程类课程教育中&#xff0c;传统编程教学方式正面临着多重痛点&#xff1a; 环境配置繁琐&#xff1a;软件工程类课程往往需要学生自行配置复杂的开发环境。但是&#xff0c;学校硬件设备条件差异、软件兼容性问题等因素&#xff0c;导致学生学习效率低下&#xf…

热销王西圣H1头戴式耳机—全平台售罄断货:揭秘抢购潮究其原因?

西圣xisem作为国内平价享轻奢的领军品牌&#xff0c;就在今年它家的头戴式蓝牙耳机性价比标杆—西圣H1&#xff0c;凭借其发烧级的千元音质、降噪与满级的旗舰配置性能&#xff0c;不仅惊艳了整个耳机圈&#xff0c;还在仅仅的几个月内&#xff0c;西圣H1头戴式耳机已经火爆断货…

python 使用gradio启动程序报错

问题一&#xff1a;localhost is not accessible 解决办法&#xff1a; export no_proxy"localhost,127.0.0.1,::1"

C#学习笔记(三)

C#学习笔记&#xff08;三&#xff09; 第 二 章 命名空间和类、数据类型、变量和代码规范二、类的组成和使用分析1. 基本概念2. 类的内容组成3. 方法的初步理解 第 二 章 命名空间和类、数据类型、变量和代码规范 二、类的组成和使用分析 1. 基本概念 类是程序的基本单元&a…

PostgreSQL中触发器递归的处理 | 翻译

许多初学者在某个时候都会陷入触发器递归的陷阱。通常&#xff0c;解决方案是完全避免递归。但对于某些用例&#xff0c;您可能必须处理触发器递归。本文将告诉您有关该主题需要了解的内容。如果您曾经被错误消息“超出堆栈深度限制”所困扰&#xff0c;那么这里就是解决方案。…

Javascript算法——二分查找

1.数组 1.1二分查找 1.搜索索引 开闭matters&#xff01;&#xff01;&#xff01;[left,right]与[left,right) /*** param {number[]} nums* param {number} target* return {number}*/ var search function(nums, target) {let left0;let rightnums.length-1;//[left,rig…

大话网络协议:从OSI七层模型说开去

时至今日,互联网已经是大家日常生活中不可或缺的一部分,购物、点餐、刷剧、网课,已经融入了我们生活的方方面面。但网络具体是怎么工作的呢? 特别是我们具体从事软件研发、ICT行业的同学,理解和掌握这个我们产品运行的基础设施尤为必要。 本文,我们会力争用最简单易懂的…

秋季猫咪疯狂掉毛,宠物空气净化器有用吗?性价比高的该怎么选?

我家猫真的是换季就变掉毛怪&#xff0c;整只猫“虚胖”了一大圈不止&#xff0c;在阳光下可以看见非常多飘在空气中的浮毛。浮毛到处乱飞&#xff0c;沉积在黑色的衣服上&#xff0c;就形成白色的薄膜。自从养猫后&#xff0c;我再也没穿过深色的衣服。 现在每天都给它梳毛&am…

Linux文件的查找和打包以及压缩

文件的查找 文件查找的用处&#xff0c;在我们需要文件但却又不知道文件在哪里的时候 文件查找存在着三种类型的查找 1、which或whereis&#xff1a;查找命令的程序文件位置 2、locate&#xff1a;也是一种文件查找&#xff0c;但是基于数据库的查找 3、find&#xff1a;针…

Vue.js 学习总结(9)—— Vue 3 组件封装技巧

1、需求说明 需求背景&#xff1a;日常开发中&#xff0c;我们经常会使用一些UI组件库诸如and design vue、element plus等辅助开发&#xff0c;提升效率。有时我们需要进行个性化封装&#xff0c;以满足在项目中大量使用的需求。错误示范&#xff1a;基于a-modal封装一个自定…

【AIGC半月报】AIGC大模型启元:2024.10(下)

【AIGC半月报】AIGC大模型启元&#xff1a;2024.10&#xff08;下&#xff09; (1) Janus&#xff08;两面神&#xff09;&#xff08;DeepSeek 1.3B多模态大模型&#xff09;(2) Stable Diffusion 3.5&#xff08;StabilityAI文生图大模型&#xff09;(3) Mochi 1&#xff08;…

Python文件操作(读取、写入、修改和删除)

目录 一、文件的读取 二、文件的写入 三、文件的修改 四、文件的删除 Python是一种功能强大的编程语言&#xff0c;文件操作是编程中常见的需求。本文将详细介绍Python中的文件操作&#xff0c;包括文件的读取、写入、修改和删除&#xff0c;帮助读者掌握Python文件操作的基…

分布式系统之异步与消息队列(MQ)(原理+代码实战一文讲清!)

异步 什么是异步 异步编程是一种编程范式&#xff0c;它允许程序在等待操作完成&#xff08;如等待网络响应、文件读写等&#xff09;时继续执行其他任务。这种编程方式对于提高程序的性能和响应性至关重要&#xff0c;尤其是在处理耗时操作或在资源受限的环境中。下面我将更…