linux:线程互斥

在这里插入图片描述

个人主页 : 个人主页
个人专栏 : 《数据结构》 《C语言》《C++》《Linux》

文章目录

  • 前言
  • 一、线程互斥
    • 问题
    • 解释
    • 互斥量的接口
  • 二、加锁的原理
  • 三、 死锁
      • 死锁四个必要条件
      • 避免死锁
  • 总结


前言

本文是对于线程互斥的知识总结


一、线程互斥

问题

我们先看下面代码。

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>

const int numbers = 3;

int ticket = 10000;
void *threadRoutine(void *args)
{
    std::string name = static_cast<char *>(args);
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1000);
            std::cout << name << "get a ticket: " << ticket << std::endl;
            ticket--;
        }
        else
        {
            break;
        }
    }
	return nullptr;
}

int main()
{
    std::vector<pthread_t> tds;
    for (int i = 0; i < numbers; ++i)
    {
        pthread_t td;
        char buff[64];
        snprintf(buff, sizeof(buff), "thread-%d", i);

        pthread_create(&td, nullptr, threadRoutine, (void *)buff);
        usleep(1000);
        tds.push_back(td);
    }

    pthread_join(tds[0], nullptr);
    pthread_join(tds[1], nullptr);
    pthread_join(tds[2], nullptr);

    return 0;
}

该代码创建三个线程-1,线程-2,线程-3,去抢夺ticket资源,当ticket从10000依次减到0时,三个线程退出。那该代码运行结果是什么呢?ticket最后会是0吗?
在这里插入图片描述
显而易见ticket最后是-1!这是为什么?三个线程在ticket为0时,不应该退出吗,ticket为什么会是-1?更奇怪的还是下图
在这里插入图片描述
ticket值尽然有相同的情况,ticket的值不应该依次递减吗?


解释

先不要着急,我们先来明确几个概念

  • 临界资源:多线程执行流共享的资源叫做临界资源(如上面代码中的ticket全局变量)

  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区(如下图红框部分的代码)

  • 互斥:任何时候,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源其保护作用

  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

在了解上面四个概念后,我们还需要了解,在C++/C中前置++ or 后置++,判断操作是原子的吗?
在这里插入图片描述
在这里插入图片描述

显然这些操作都不是原子的,判断在汇编指令是要先判断,再依据判断结果跳转执行流,后置–是先从内存中读取变量的值放到寄存器中,再对寄存器中的值-1, 最后将寄存器中的值放回变量中。

现在知道了上述知识,我们可以来理解为什么ticket会不变和变为-1。

我们现将这些汇编指令分别表明为步骤1,步骤2…
在这里插入图片描述
我们先来解释为什么ticket的值可能不变。
在这里插入图片描述
当线程thread-0执行步骤3,将内存中icket的值34,读取到寄存器中;线程thread-0被线程thread-1切换,寄存器中的34,会作为线程thread-0的上下文数据,被线程thread-0保留,此时线程thread-1执行步骤3,将内存中icket的值34,读取到寄存器中;线程thread-1被线程thread-2切换,同理寄存器中的34作为线程thread-1的上下文数据被保留;此时线程thread-2执行步骤3,将内存中icket的值34,读取到寄存器中,再执行步骤4,将寄存器中的34变为33,在执行步骤5,将寄存器中的33放回到内存中ticket处,此时ticket = 33;线程thread-2打印ticket的值;线程thread-2被线程thread-0切换,要恢复线程thread-0的上下文数据,寄存器中存储的是34,线程thread-0在执行步骤4,将寄存器中的34变为33,在执行步骤5,将寄存器中的33拷贝到内存中ticket处,ticket = 33,线程thread-0打印ticket的值为33;同理线程thread-2最后也会打印ticket的值为33。这就是线程0,1,2都会打印33的原因。
在这里插入图片描述

明白了为什么ticket会打印3次33。那ticket为什么会变为-1,就好理解了。
我们假定此时ticket = 1;线程thread-0执行步骤1( 1 > 0)判断为真,被线程thread-1切换,判断结果作为线程thread-0的上下文数据被保存;线程thread-1也指向步骤1( 1 > 0)判断为真,在执行步骤2,步骤3,步骤4,步骤5,从内存中读取ticket的值(ticket = 1),再在寄存器内将1 -> 0,再将0拷贝到内存ticket处(ticket= 0),线程tithread-1被线程thread-2切换,线程thread-2执行步骤1( 0 > 0)判断为假,结束循环;线程thread-2被线程thread-0切换,线程thread-0执行步骤2,步骤3,步骤4,步骤5,从内存中读取ticket的值(ticket = 0),再在寄存器内将0 -> -1,再将-1拷贝到内存中ticket处(ticket = -1)。这就是ticket为什么会是-1的原因。

这就是我们多线程访问共享数据而导致数据不一致问题,那如何解决呢?

要解决以上数据不一致问题,就要保证只能有一个执行流在临界区执行代码。而这就是锁,linux上提供的这把锁叫做互斥量。
在这里插入图片描述


互斥量的接口

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

  • 静态分配
    在这里插入图片描述

  • 动态分配
    在这里插入图片描述
    mutex:要初始化的互斥量;
    attr:用于设置互斥锁的属性(传递 NULL 作为 attr 参数的值,那么互斥锁会使用默认的属性进行初始化。)

销毁互斥量
在这里插入图片描述
如果成功销毁互斥锁,则返回0;
如果发生错误,则返回错误码(EBUSY:在尝试销毁一个正在使用的互斥锁时,通常会返回这个错误;EINVAL:传递个函数的mutex指针无效)
需要注意的是:

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

互斥量加锁和解锁

在这里插入图片描述
加锁成功,返回0;加锁失败,返回错误码(如EDEADLK, EINVAL, EBUSY)
当互斥锁已经被其它线程锁定时,调用pthread_mutex_lock的线程通常会被阻塞,直到互斥锁被解锁。如果不想线程申请锁失败被阻塞,可以使用pthread_mutex_trylock函数。

在这里插入图片描述
加锁成功,返回0; 加锁失败,返回错误码(如EBUSY:当前互斥锁被其他线程锁定)
pthread_mutex_trylock不会让调用线程在互斥锁不可用时进入阻塞状态,这使得可以用轮询的方式来申请锁。

在这里插入图片描述
成功解除锁,返回0;解除锁失败,返回错误码(如传递给函数的mutex指针无效,解除一个未由当前线程锁定的锁)

现在我们对开始的代码进行改进

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>

const int numbers = 3;
// 定义锁
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;

int ticket = 10000;
void *threadRoutine(void *args)
{
    std::string name = static_cast<char *>(args);
    while (true)
    {
    	//加锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            std::cout << name << "get a ticket: " << ticket << std::endl;
            ticket--;
            // 解锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
        	// 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

int main()
{
    std::vector<pthread_t> tds;
    for (int i = 0; i < numbers; ++i)
    {
        pthread_t td;
        char buff[64];
        snprintf(buff, sizeof(buff), "thread-%d", i);

        pthread_create(&td, nullptr, threadRoutine, (void *)buff);
        usleep(1000);
        tds.push_back(td);
    }

    pthread_join(tds[0], nullptr);
    pthread_join(tds[1], nullptr);
    pthread_join(tds[2], nullptr);

    return 0;
}

在这里插入图片描述
现在ticket == 1时,程序退出。

二、加锁的原理

pthread_mutex_t的结构如下:
在这里插入图片描述

我们先简单的将mutex这个结构体理解为一个int,将mutex = 1视为锁资源空闲,将mutex = 0视为锁资源已经被占用。
在这里插入图片描述
当一个线程要加锁时,其要先执行movb指令,将0移动到%al寄存器中(表示未持有锁),再执行xchgb指令,将mutex中的值与%al寄存器交换,如果%al寄存器中的值大于0,表示加锁成功,可以执行临界区代码;%al寄存器的值小于等于0,表示加锁失败,要挂起等待其它持有该锁的线程释放锁,再执行goto语句,重新申请锁。
在这里插入图片描述
当一个线程释放锁时,其要执行movb指令,将1移动到mutex处(表示锁资源空闲),再唤醒挂起等待的线程;

看了上面内容,我们可能还有点疑惑,为什么pthread_mutex_lock函数就是原子的呢?下面让我们已两个线程1,2申请锁为列,来理解。
我们假定当前有一个空闲的锁mutex,线程1执行movb指令,将0移动到%al寄存器中,线程1被线程2切换,线程1保存%al寄存器中的内容0;线程2执行movb指令,将0移动到%al寄存器中,再执行xchgb指令,将mutex的值与%al寄存器中的值交换(mutex = 0, %al = 1),线程2被线程1切换,%al寄存器中的内容1作为线程2的上下文数据被保存,线程1回复上下文数据(%al = 0),再执行xchgb指令,交换mutex和%al寄存器的内容(%al = 0, mutex = 0),再执行if判断为假,线程1挂起等待;线程1被线程2切换,回复上下文数据(%al = 1),线程2执行if判断为真,线程2执行临界区代码。

在这里插入图片描述

现在我们就可以理解,为什么pthread_mutex_lock函数是原子的了。

三、 死锁

死锁是指在一组进程中各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
如下图所示:
在这里插入图片描述
线程A,B只有同时拥有锁1,锁2才能访问临界区代码,此时线程A拥有lock1,申请lock2,线程B拥有lock2,申请lock1,线程A,B都会因为所申请的资源被其它线程所占有而等待,这就是死锁。

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获取资源保持不放
  • 不剥夺条件:一个执行流已获取的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间现成一个头尾相接的循环等待资源的关系(如上图,线程A需要线程B持有的lock2,线程B需要线程A持有的lock1,线程A,B形成头尾相接的循环等待)

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致(如要求线程A,B都因先申请lock1,再申请lock2)
  • 避免锁未释放的场景(如将pthread_mutex_unlock误写成pthread_mutex_lock,从而导致只有一个线程造成的死锁)
  • 资源一次性分配

总结

以上就是我对于线程互斥的总结。

在这里插入图片描述

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

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

相关文章

连锁收银系统如何降低连锁经营的税务成本

空中分账是指将总部和各门店之间的财务往来通过虚拟账户进行结算&#xff0c;而非实际资金流动。这种方式可以加强连锁企业的管控&#xff0c;同时在合规的前提下降低税务成本&#xff0c;具体有以下优势&#xff1a; 加强管控&#xff1a; 连锁门店收银统一进入连锁品牌空中账…

Springboot和Spring Cloud版本对应

Spring在不断地升级&#xff0c;各个版本存在一些不兼容的地方&#xff0c;为了避免出现问题&#xff0c;最好注意使用正确的版本。 官网的对应关系&#xff1a;https://start.spring.io/actuator/info 如下图&#xff1a; 下面附一下创建项目的工具&#xff1a; Spring官方…

深入理解模板进阶:掌握C++模板的高级技巧

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生 &#x1f648;个人主页&#x1f389;&#xff1a;GOTXX &#x1f43c;个人WeChat&#xff1a;ILXOXVJE &#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&…

禅道二次开发——创建需求

获取Token 官网参考 https://www.zentao.net/book/api/setting-369.html post http://xxx:8442/zentaopms/www/api.php/v1/tokensbody {"account": "xxx", "password": "xxx"}结果如下图 创建需求 官网参考 https://www.zentao.…

DBA面试题:MySQL缓存池LRU算法做了哪些改进?

下图是MySQL&#xff08;MySQL5.7版本&#xff09;体系架构图 MySQL的InnoDb Buffer Pool 缓冲池是主内存中的一个区域&#xff0c;用来缓存InnoDB在访问表和索引时的数据。对于频繁使用的数据可以直接从内存中访问&#xff0c;从而加快处理速度。如果一台服务器专用作MySQL数据…

Footprint Analytics 强势入局 AI 推出全新投研工具

作者&#xff1a;lesleyfootprint.network 3 月 15 日&#xff0c;链上数据平台 Footprint Analytics 宣布入局 AI&#xff0c;推出了旗下首款 AI 投研工具—— Pea.AI。 作为专门服务于区块链行业的 AI 平台&#xff0c;Pea.AI 旨在提升加密资产投资领域的数据流动性和知识…

c语言实现https客户端 源码+详细注释(OpenSSL下载,visual studio编译器环境配置)

OpenSSL的下载和环境配置 请参考&#xff1a;openssl下载安装教程 步骤&#xff1a;官网下载->安装到选定目录->配置环境变量->打开命令窗口检查是否安装成功 注意&#xff1a; 打开命令窗口&#xff08;快捷键winr,在弹出窗口内输入cmd按回车&#xff09;&#xff0…

leetcode代码记录(长度最小的子数组

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 连续 子数组 [numsl, numsl1, …, numsr-1, numsr] &#xff0c;并返回其…

外贸业务员如何说服老板拿到更低价

小伙伴问我说如何说服老板给到更好的价格&#xff0c;这个问题呢我在这里说一下我的观点 第一你需要去分析这个客户到底值不值得我们去给他花更多的一些心思&#xff0c;因为客户想要的这个价格既然已经突破了公司的价格标准了&#xff0c;说明他的价格要的非常的低&#xff0…

每日一题——LeetCode1704.判断字符串的两半是否相等

方法一 计数 从头到尾简历字符串&#xff0c;判断字符是否是元音字符&#xff0c;如果是再判断i的值是字符串前一半还是后一半&#xff0c;前一半sum1&#xff0c;后一半sum2 var halvesAreAlike function(s) {const h "aeiouAEIOU";let sum1 0, sum2 0;for (l…

TypeScript:typescript的安装与运行

TypeScript&#xff1a;typescript的安装与运行 1 安装方式 -g全局安装TypeScript&#xff1a; npm install -g typescript2 运行方式 &#xff08;1&#xff09;ts编译成js&#xff0c;使用node命令运行js文件 打开vscode&#xff0c;进入ts文件所在目录下并打开终端term…

微信小程序 canvas层级过高覆盖原生组件

一、背景 微信小程序中使用signature第三方插件完成签名效果&#xff0c;但真机调试时发现canvas层级过高遮挡了按钮 二、具体问题 问题原因&#xff1a;签名后点击按钮无法生效 问题代码&#xff1a; <template><view class"sign_page" v-cloak>&l…

MATLAB编译器配置:MinGW

使用 MATLAB 2022b版本&#xff0c;查询编译器时如上&#xff0c;想安装个MinGW编译器&#xff0c;自带的附加资源管理不好使&#xff0c;只能换个别的法子&#xff0c;经过一些参考&#xff0c;总结如下。 步骤1.在这里下载一个MinGW.最新版本是10.3.0.然后默认安装&#xff…

【好用】Star超36.8k,一个的免费通用数据库管理工具

关于数据库管理工具&#xff0c;大家可能都在用SQLyog、Navicat、MySQL-Front、SQL Studio、MySQL Workbench等等&#xff0c;这些管理工具不是不好用&#xff0c;就是要变魔术才可以用&#xff0c;今天 V 哥给大家推荐一个即好用&#xff0c;又免费的可视化通用数据库管理工具…

手机备忘录怎么导出到电脑,如何将手机备忘录导出到电脑

备忘录是我们日常生活和工作中常用的工具之一&#xff0c;我们可以在手机上轻松地记录重要的事务、想法和灵感。然而&#xff0c;在某些情况下&#xff0c;我们可能需要将手机备忘录导出到电脑进行更详细的整理和管理。那么&#xff0c;手机备忘录怎么导出到电脑&#xff0c;如…

如何设计一个kafka(理解)

因业务需要而对消息中间件的频繁使用后&#xff0c;每次总会问自己一个问题&#xff1a;kafka为什么快&#xff1f;然后再去背一背八卦找找答案。直到近日终于能站在一个新奇的角度理解kafka&#xff0c;且积累的各种细节串通了起来&#xff0c;实属惊喜。 回到最开始的问题&am…

VS Code安装

VS Code 安装 一、下载 进入VS Code官网&#xff1a;https://code.visualstudio.com&#xff0c;点击 DownLoad for Windows下载windows版本 Stable&#xff1a;稳定版Insiders&#xff1a;内测版 二、安装 双击安装包&#xff0c;选择我同意此协议&#xff0c;再点击下一步 …

【办公类-22-15】周计划系列(5-6)“周计划-06 周计划打印pdf(docx删除内容转PDF)“ (2024年调整版本)

作品展示 背景需求&#xff1a; 前期用docx&#xff08;删除第一页反思部分内容&#xff09;转PDF转png&#xff08;第一页&#xff09;的方式获得上传网页用的图片。 【办公类-22-14】周计划系列&#xff08;5-5&#xff09;“周计划-05 上传周计划png&#xff08;docx转PDF…

2. IS-IS 基础实验

2.1 IS-IS 配置实验 2.1.1 实验介绍 2.1.1.1 学习目标 1. 实现 IS-IS 协议基本配置 2. 实现 IS-IS 协议 DIS 优先级修改 3. 实现 IS-IS 协议网络类型修改 4. 实现 IS-IS 协议外部路由引入 5. 实现 IS-IS 接口 cost 修改 6. 实现 IS-IS 路由渗透配置 2.1.1.2 实验组网介…

【每日一题】1793. 好子数组的最大分数-2024.3.19

题目&#xff1a; 1793. 好子数组的最大分数 给你一个整数数组 nums &#xff08;下标从 0 开始&#xff09;和一个整数 k 。 一个子数组 (i, j) 的 分数 定义为 min(nums[i], nums[i1], ..., nums[j]) * (j - i 1) 。一个 好 子数组的两个端点下标需要满足 i < k < …