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

news2024/10/23 22:30:53

目录

线程互斥

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

数据不一致问题

深度理解锁

原理角度理解:

实现角度理解:

线程同步

条件变量

测试代码

生产消费模型

生产消费模型概念

编写生产消费模型

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;
};

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

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

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

相关文章

Dell服务器导入idrac 授权文件 (适用iDRAC7、iDRAC8、iDRAC9)

iDRAC Enterprise、iDRAC Datacenter 和 CMC Enterprise 的 30 天试用许可证,供熟悉高级功能集,例如使用虚拟控制台等 OpenManage Enterprise Advanced 或 Advanced+ 许可证支持高级功能,例如自动部署、服务器配置合规性和激活可用插件,如 OpenManage Enterprise Power Ma…

MySQL 数据库迁移至达梦 DM8 常见问题

目录 如何让迁移到 DM 的表名大小写和 MySQL 保持一致 MySQL 迁移到 DM 报错&#xff1a;列[NAMES]长度超出定义 MySQL 迁移到 DM 报错&#xff1a;记录超长 索引错误 DM大小写敏感配置 表空间 新建用户 用户与模式的关系 省略模式名的优势 实际操作 如何让迁移到 DM…

知识图谱的概念、特点及应用领域(详解)

目录 什么是知识图谱&#xff1f; 二、特点 三、应用领域 什么是知识图谱&#xff1f; 知识图谱&#xff08;Knowledge Graph&#xff09;是一种将知识进行结构化、组织和表示的方法&#xff0c;它利用图形模型表示事物之间的关系和属性。知识图谱通过节点&#xff08;实体&…

qt QWidget详解

一、概述 QWidget是容器组件&#xff0c;继承自QObject类和QPaintDevice类。能够绘制自己和处理用户输入&#xff0c;是QT中所有窗口组件类的父类&#xff0c;是所有窗口组件的抽象&#xff0c;每个窗口组件都是一个QWidget&#xff0c;QWidget类对象常用作父组件或顶级组件使…

T113 内核中 adbd相关配置1

准备工作 1. 配置 系统&#xff1a;ubuntu24.04docker&#xff08;ubuntu18.04&#xff09; 软件vscode, sdk:Tina-linux&#xff08;BingPi-M2&#xff09; 2. 构建环境直接使用自带的 source ./build/envsetup.sh lunch 选择 6 编译开启16线程 make -j16boot编译 mboot 打包…

关于jmeter中没有jp@gc - response times over time

1、问题如下&#xff1a; jmeter没有我们要使用的插件 2、解决方法&#xff1a; 选择下面文件&#xff0c;点击应用&#xff1b; 3、问题解决 ps&#xff1a;谢谢观看&#xff01;&#xff01;&#xff01;

力扣 简单 746.使用最小花费爬楼梯

文章目录 题目介绍题解 题目介绍 题解 思路分析&#xff1a; 确定dp数组以及下标的含义&#xff1a;dp[i]的定义为到达第i台阶所花费的最少体力。确定递推公式&#xff1a;可以有两个途径得到dp[i]&#xff0c;一个是dp[i-1] 一个是dp[i-2]。dp[i - 1] 跳到 dp[i] 需要花费 d…

玩转springboot之springboot异步执行

springboot异步执行 使用EnableAsync开启异步执行 在接口方法上使用Async注解进行标注&#xff0c;该接口是一个异步接口 自定义异步线程执行器 Configuration public class CustomAsyncConfigurer implements AsyncConfigurer {Overridepublic Executor getAsyncExecutor() {T…

WebGL编程指南 - 颜色与纹理

将顶点的其他&#xff08;非坐标&#xff09;数据——如颜色等——传入顶点着色器。 发生在顶点着色器和片元着色器之间的从图形到片元的转化&#xff0c;又称为图元光栅化 &#xff08;rasterzation process&#xff09;。 将图像&#xff08;或称纹理&#xff09;映射到图形…

C++笔记---哈希表

1. 哈希的概念 哈希(hash)又称散列&#xff0c;是一种组织数据的方式。从译名来看&#xff0c;有散乱排列的意思。 本质就是通过哈希函数把关键字Key跟存储位置建立一个映射关系&#xff0c;查找时通过这个哈希函数计算出Key存储的位置&#xff0c;进行快速查找。 STL中的un…

推荐IDE中实用AI编程插件,目前无限次使用

插件介绍 一款字节跳动推出的“基于豆包大模型的智能开发工具” 以vscode介绍【pycharm等都可以啊】&#xff0c;这个插件提供智能补全、智能预测、智能问答等能力&#xff0c;节省开发时间 直接在IDE中使用&#xff0c;就不用在网页中来回切换了 感觉还可以&#xff0c;响应速…

Excel表格如何修改“打开密码”,简单几步,轻松搞定

在保护Excel文件的安全性时&#xff0c;设置打开密码是常见且有效的方式。然而&#xff0c;有时我们需要修改已经设置的打开密码&#xff0c;以确保文件安全性或更新密码信息。今天小编来分享一下修改Excel文件打开密码的方法&#xff0c;操作简单&#xff0c;一起来看看吧&…

设置OpenAI API的环境变量

获取openai API 密钥 https://platform.openai.com/api-keys 设置环境变量 为什么不在代码中直接写入&#xff0c;而是设置环境变量&#xff1f; 安全性&#xff1a;将 API 密钥存储在环境变量中&#xff0c;而不是直接写在代码中&#xff0c;可以降低泄露密钥的风险。易于…

第二期:第15节,beep 大海

首先是 代码的编写&#xff1a; 里面已经有了解释了。 1 /*2 * main.c3 *4 * Created on: 2023-3-85 * Author: pengdan6 */7 #include "exynos_4412.h"89 void delay_ms(unsigned int num)10 {11 int i,j;12 for(inum; i>0;i--)13 …

『完整代码』坐骑召唤

创建一个按钮 作为召唤/消失坐骑的开关 将预制体放入指定文件夹 命名为Mount01 创建脚本并编写&#xff1a;CallMount.cs using UnityEngine; using UnityEngine.UI; public class CallMount : MonoBehaviour{public Button callBtn;GameObject mountPrefab;GameObject mountIn…

[项目详解][boost搜索引擎#1] 概述 | 去标签 | 数据清洗 | scp

目录 一、前言 二、项目的相关背景 三、搜索引擎的宏观原理 四、搜索引擎技术栈和项目环境 五、正排索引 VS 倒排索引--原理 正排索引 分词 倒排索引 六、编写数据去除标签和数据清洗模块 Parser 1.数据准备 parser 编码 1.枚举文件 EnumFile 2.去标签ParseHtml(…

使用Vscode配置ftp连接远程服务器(上传本地文件)

1.安装插件 扩展商店搜sftp,点击进行安装。 2.配置json文件 crtl+shift+p 输入ftp配置命令 sftp:config {"name": "My Server", //设置名字"host": "localhost"</

android app执行shell命令视频课程补充android 10/11适配-千里马android

(https://blog.csdn.net/learnframework/article/details/120103471) https://blog.csdn.net/learnframework/article/details/120103471 hi&#xff0c;有学员在学习跨进程通信专题课程时候&#xff0c;在实战app执行一个shell命令的项目时候&#xff0c;对课程本身的android …

JVM、字节码文件介绍

目录 初识JVM 什么是JVM JVM的三大核心功能 JVM的组成 字节码文件的组成 基础信息 Magic魔数 主副版本号 其它基础信息 常量池 字段 方法 属性 字节码常用工具 javap jclasslib插件 阿里Arthas 初识JVM 什么是JVM JVM的三大核心功能 1. 解释和运行虚拟机指…

js实现点击随机点名效果

代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </he…