_Linux多线程-线程互斥篇

news2024/11/23 12:22:57

文章目录

  • 1. 进程线程间的互斥相关背景概念
  • 2. 互斥量mutex
  • 3. 互斥量的接口
    • 初始化互斥量
    • 销毁互斥量
    • 互斥量加锁和解锁
  • 4. 互斥量---锁
    • 静态分配(初始化)
    • 动态分配(初始化)
  • 5. 互斥量实现原理探究
  • 6. 总结:

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

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

2. 互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

例子:操作共享变量会有问题的售票系统代码

  • 代码块
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>

using namespace std;

//买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!

void *getTickets(void *args)
{
    (void*)args;
    // pthread_self() 获取该线程的id
    while(true)
    {
        if(tickets>0)
        {
            usleep(1000);
            printf("%p: %d\n", pthread_self(), tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    // 创建一个线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, nullptr, getTickets, nullptr);
    pthread_create(&tid2, nullptr, getTickets, nullptr);
    pthread_create(&tid3, nullptr, getTickets, nullptr);
    

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    return 0;
}

我们理想下:最后票数为0。

  • 运行结果
    在这里插入图片描述
    结果竟然是-1。

  • 为什么可能无法获得争取结果?

    • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
    • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
    • - -ticket 操作本身就不是一个原子操作。
        1. 读取数据到cpu内的寄存器中
      • 2.CPU内部进行计算**- -**
      • 3.将结果写回内存中
        • 在这其中很容易进行到一半就挂起;让其它线程来再执行。

在这里插入图片描述
—(图片摘自相关教材)

  • – 操作并不是原子操作,而是对应三条汇编指令:
    • load :将共享变量ticket从内存加载到寄存器中。
    • update : 更新寄存器里面的值,执行-1操作。
    • store :将新值,从寄存器写回共享变量ticket的内存地址。
  • 要解决以上问题,需要做到三点:
    • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
    • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
    • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
  • 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
    在这里插入图片描述

3. 互斥量的接口

初始化互斥量

初始化互斥量有两种方法:

  • 方法1,静态分配:
 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER
  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数: mutex:要初始化的互斥量 attr:NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex)

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

4. 互斥量—锁

改进上面的售票系统:

静态分配(初始化)

  • 代码块:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cassert>

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配

// 买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!

void *getTickets(void *args)
{
    (void *)args;
    // pthread_self() 获取该线程的id
    while (true)
    {
        int n = pthread_mutex_lock(&mutex); // 上锁
        assert(n == 0);
        if (tickets > 0)
        {
            usleep(1000);
            printf("%p: %d\n", pthread_self(), tickets);
            tickets--;
            pthread_mutex_unlock(&mutex); // 解锁
            assert(n == 0);
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            assert(n == 0);
            break;
        }

        // 抢完票,其实还需要后续的动作
        usleep(rand() % 200);
    }
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid() ^ 2023);
    // 创建一个线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, nullptr, getTickets, nullptr);
    pthread_create(&tid2, nullptr, getTickets, nullptr);
    pthread_create(&tid3, nullptr, getTickets, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    pthread_mutex_destroy(&mutex); // 销毁锁
    return 0;
}

  • 结果达到理想状态:

在这里插入图片描述

动态分配(初始化)

  • 代码块:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cassert>

using namespace std;

// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配

// 买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!

#define THREAD_NUM 5

class ThreadData
{
public:
    ThreadData(const string &n, pthread_mutex_t *pm) : tname(n), pmtx(pm)
    {
    }

    string tname;
    pthread_mutex_t *pmtx;
};
void *getTickets(void *args)
{
    // pthread_self() 获取该线程的id
    ThreadData *mutex = (ThreadData *)args;
    while (true)
    {
        int n = pthread_mutex_lock(mutex->pmtx); // 上锁
        assert(n == 0);
        if (tickets > 0)
        {
            usleep(1000);
            printf("%s: %d\n", mutex->tname.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(mutex->pmtx); // 解锁
            assert(n == 0);
        }
        else
        {
            pthread_mutex_unlock(mutex->pmtx); // 解锁
            assert(n == 0);
            break;
        }

        // 抢完票,其实还需要后续的动作
        usleep(rand() % 200);
    }

    delete mutex;
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid() ^ 2023);
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);
    pthread_t t[THREAD_NUM];
    // 创建线程
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        string name = "thread ";
        name += to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mutex);
        pthread_create(t + i, nullptr, getTickets, td);
    }
    // 等待线程
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        pthread_join(t[i], nullptr);
    }

    pthread_mutex_destroy(&mutex); // 销毁锁
    return 0;
}

  • 结果展示:
    在这里插入图片描述

5. 互斥量实现原理探究

  • 经过上面的例子我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
    在这里插入图片描述
    交换的现象: 内存<->%al 做交换
    交换的本质: 共享<->私有(线程自己的上下文)
    即保证了锁里面‘1’ 的唯一性。
    在这里插入图片描述

6. 总结:

  • 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响;避免方法:

    • 加锁保护:加锁的时候,一定要保证加锁的粒度,越小越好!!
  • 加锁就是串行执行。

    • 执行临界区代码一定是串行的!
  • 加锁了之后,线程在临界区中,会切换,没有时序问题。

    • 原子性的体现
    • 虽然被切换了,但是你是持有锁被切换的, 所以其他抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,所以,也不会让其他线程进入临界区,就保证了临界区中数据一致性!
  • 我是一个线程,我不申请锁,就是单纯的访问临界资源! 这是一种错误的编码方式。

  • 在没有持有锁的线程看来,对它最有意义的情况只有两种:

      1. 线程1没有持有锁(什么都没做)
      1. 线程1释放锁(做完),此时我可以申请锁!
  • 要访问临界资源,每一个线程都必须现申请锁,每一个线程都必须先看到同一把锁并且可以访问它,即锁本身就是一种共享资源。那么谁来保证锁的安全呢??为了保证锁的安全,申请和释放锁,必须是原子滴

  • 锁自身保证的(上面的伪代码)—设计者这样设计滴

  • 在执行流视角,是如何看待CPU上面的寄存器的?

    • CPU内部的寄存器本质,叫做当前执行流的上下文! !寄存器们,空间是被所有的执行流共享的,但是寄存器的内容,是被每一个执行流私有的! 上下文!

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

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

相关文章

【随即森林模型】

随机森林模型的基本原理和代码实现 集成模型简介 集成学习模型是机器学习非常重要的一部分。 集成学习是使用一系列的弱学习器&#xff08;或称之为基础模型&#xff09;进行学习&#xff0c;并将各个弱学习器的结果进行整合从而获得比单个学习器更好的学习效果的一种机器学习…

嵌入式设备中可以使用SQLite3吗?

摘要&#xff1a;数据库是用来存储和管理数据的专用软件&#xff0c;使得管理数据更加安全&#xff0c;方便和高效。数据库对数据的管理的基本单位是表(table)&#xff0c;在嵌入式linux中有时候它也需要用到数据库&#xff0c;听起来好难&#xff0c;其实就是几个函数&#xf…

论文精读:Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields ∗

姿态估计openpose系列算法解读 姿态估计任务 姿态估计任务首先需要检测出人体的各个关键点,将人体关键点进行拼接。 任务的困难有,首先,对于关键点检测任务,需要处理遮挡的问题,在拼接的过程中,需要处理多人的情况,即不能将不同人的关键点进行拼接。 标注数据信息 COCO…

linux系统中利用QT实现音乐播放器的功能

大家好&#xff0c;今天主要和大家聊一聊&#xff0c;如何使用QT中的音乐播放器的功能与方法。 目录 第一&#xff1a;音乐播放器基本简介 第二&#xff1a;应用具体代码实现 第三&#xff1a;在源代码mainwindow.cpp中的实现 第四&#xff1a;程序运行效果 第一&#xff…

1.1计算机工作过程(超详细)

文章目录一、计算机组成框图二、思维导图三、部件剖析&#xff08;1&#xff09;存储器&#xff08;2&#xff09;运算器&#xff08;3&#xff09;控制器四、案例剖析&#xff08;重点&#xff09;&#xff08;1&#xff09;a2&#xff08;2&#xff09;a*b&#xff08;3&…

关于 国产麒麟系统上长时间运行Qt程序.xsession-erros文件占满磁盘导致无法写入 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/128660728 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…

[强网杯 2019]随便注

目录 信息收集 方法一&#xff1a;堆叠注入 方法二&#xff1a;MySQL预处理 语法 payload 方法三&#xff1a;handler 知识点 语法 payload 信息收集 1 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version f…

开发中常用的Spring注解

一.IOC容器 Configuration ConpoentScan CompoentScans Bean Import DependsOn Lazy Compoent Repository Service Controller Autowired Qualifier 二.AOP切面 Aspect Pointcut Before After AfterReturning AfterThrowing Around 三.事务声明 Transac…

nacos一:服务注册

为什么用nacos: Eureka需要自己搭建项目&#xff0c;nacos下载后&#xff0c;就可以直接访问web界面,自带负载均衡 Nacos可以 1替代eureka做服务注册中心 2替代Config做服务配置中心 使用 一&#xff1a; 1 下载nacos,在bin目录下打开cmd窗口&#xff0c;输入startup.cmd -m s…

100 亿美元!微软豪赌 AI,OpenAI 渗透 GitHub、Office、Bing

OpenAI 这把 ChatGPT 的火还在持续地燃烧&#xff01;作者 | 唐小引出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;今天&#xff0c;据路透社援引 Semafor 消息报道&#xff0c;微软正在计划向 OpenAI 再次投资 100 亿美元&#xff0c;如果合作达成&#xff0c;微…

【STM32学习】SysTick定时器(嘀嗒定时器)

SysTick定时器一、参考资料二、时钟源选择与定时时间计算1、时钟源选择2、定时时间计算三、SysTick_Handler中断服务函数一、参考资料 嘀嗒定时器&#xff1a;时钟源、寄存器 二、时钟源选择与定时时间计算 结合正点原子的代码进行说明&#xff1a; 1、时钟源选择 从上图可以发…

通讯录的实现(详解)(后附完整源代码)

通讯录的实现一.所需要的功能二.大致菜单三.创建通讯录四.增加联系人五.显示联系人六.查找联系人七.删除联系人八.修改联系人一.所需要的功能 对于通讯录来说&#xff0c;我们需要它实现以下几个功能。 1.人的信息&#xff1a;姓名年龄性别电话地址。 2.可以存放100个人的信息…

VMware Workstation Pro 16安装Windows 11

1&#xff1a;首先在机器中安装VMware Workstation Pro。 2&#xff1a;准备Windows 11的安装镜像。 3&#xff1a;安装Windows 11的系统要求&#xff0c;这个很关键不满足条件无法安装&#xff0c;其中我们只需要注意系统固件和TPM这两项就行。 4&#xff1a;运行VMware Wor…

使用SQL4Automation让CodeSYS连接数据库

使用SQL4Automation让CodeSYS连接数据库 摘要&#xff1a;本文旨在说明面向CodeSYS的数据库连接方案SQL4Automation的使用方法。 1.SQL4Automation简介 1.1.什么是SQL4Automation SQL4Automation是一套工业用途的软件解决方案&#xff0c;它主要的功能就是为PLC和机器人控制提…

王道操作系统笔记(一)———— 计算机系统概述

文章目录一、操作系统基本概念1.1 基本概念1.2 四大特征1.3 目标和功能二、操作系统的分类与发展三、操作系统的运行环境3.1 运行机制3.2 中断和异常3.3 系统调用四、操作系统的体系结构4.1 宏内核与微内核4.2 分层结构4.3 模块化4.4 外核五、操作系统引导六、虚拟机一、操作系…

nacos2.x集群版搭建

1. 预备环境准备 请确保是在环境中安装使用: 64 bit OS Linux/Unix/Mac&#xff0c;推荐使用Linux系统。--这里使用linux系统64 bit JDK 1.8&#xff1b;下载. 配置。Maven 3.2.x&#xff1b;下载. 配置。3个或3个以上Nacos节点才能构成集群。官网地址:集群部署说明 2、服务器…

Android 深入系统完全讲解(12)

11 跟踪一个服务&#xff0c;直接找到驱动实现 如果说我自己学习整个系统&#xff0c;直到底层驱动的方法&#xff0c;我想说的就是我常用的就是跟踪震动这个模块&#xff0c;而为什么是这个&#xff0c;主要是简单&#xff0c;但是又是从上到下都具备&#xff0c;对于学习系统…

【OpenCV】拾遗

前言 本篇博客主要是总结OpenCV使用过程中遇到的一些问题&#xff0c;便于以后参考。 以下所有内容均基于VS2015 OpenCV_v4.5.1 及 VS Code MinGW_v4.3.5 CMake_v3.20.0 OpenCV_v4.5.1&#xff0c;前者的配置教程可以参考这个链接&#xff0c;后者的配置教程可以参考这个链…

2022年衣物清洁行业市场报告:洗衣液等四大高增长类目分析

随着人们经济水平的提高以及消费观念的升级&#xff0c;当前个护家清用品逐渐朝品质化、精细化、个性化的方向发展&#xff0c;类目衍生更替更频繁、迭代速度更快。 得益于庞大的人口规模&#xff0c;个护家清产品规模巨大&#xff0c;衣物清洁行业虽增速放缓但仍在个护家清行…

OpenFoam收缩扩张喷管(拉瓦尔喷管)边界条件的设置

简介 收缩扩张喷管&#xff08;也成拉瓦尔喷管&#xff09;广泛应用于火箭推进。将其流动特性定性描述如下&#xff1a; &#xff08;1&#xff09;当入口流量较小时&#xff0c;不出现雍塞&#xff0c;流速先增大后减小&#xff0c;全程为亚声速。出口压力即为大气压&#x…