linux:线程互斥

news2025/2/25 9:11:53

在这里插入图片描述

个人主页 : 个人主页
个人专栏 : 《数据结构》 《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,从而导致只有一个线程造成的死锁)
  • 资源一次性分配

总结

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

在这里插入图片描述

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

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

相关文章

Division by Invariant Integers using Multiplication

在处理器中&#xff0c;整数除法的成本通常是整数乘法的几倍&#xff1a; 流水线式的组合乘法器通常在不到10个周期内完成操作&#xff1b;而对于整数除法则没有硬件支持&#xff0c;或者使用的迭代除法器比乘法器慢几倍。 表 1.1 比较了一些处理器上乘法和除法的时间。这张表…

c++多长时间会被Python或者其他语言取代?

c多长时间会被Python或者其他语言取代&#xff1f; 如果不考虑市场因素&#xff0c;C#今天就可以取代C。 自.NET跨平台至今&#xff0c;C能做的工作&#xff0c;C#都能做了&#xff0c;且性能差别不大。 在C最有优势的嵌入式UI方面&#xff0c;C#可以拿出Avalonia替代QT。用 …

存储器的层次结构和局部性原理

前言 大家好我是jiantaoyab&#xff0c;这是我所总结作为学习的笔记第19篇&#xff0c;在这里分享给大家&#xff0c;这篇文章讲存储器的一部分内容。 存储器的层次结构 SRAM 静态随机存取存储器的芯片&#xff0c;SRAM 之所以被称为“静态”存储器&#xff0c;是因为只要处…

MYSQL概念和编译安装

目录 一、数据库概述 1.1数据 1.2表 1.3数据库 总结&#xff1a; 2.数据库管理系统&#xff08;DBMS&#xff09; 3.DBMS工作模式 4.数据库系统原理 二、数据库发展史 三、主流数据库 四、关系型数据库和非关系型数据库 1.关系型数据库 2.非关系数据库 MYSQL数据…

输出菱形(*)--c语言

//输出菱形 #include<stdio.h>int main(){//上int line0;scanf("%d",&line);int i0;for(i0;i<line;i){int j0;//输出空格for(j0;j<line-1-i;j){printf(" ");}//输出*号for(j0;j<2*i1;j){printf("*");}printf("\n")…

Redisson 分布式锁原理分析

Redisson 分布式锁原理分析 示例程序 示例程序&#xff1a; public class RedissonTest {public static void main(String[] args) {Config config new Config();config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6379"…

【开发环境】Ubuntu 18.04 搭建 QT编译环境详细步骤 【亲测有效】

目录 1 查看Ubuntu系统中Qt版本 2 下载Ubuntu系统Qt版本安装包 3 Qt安装 3.1 Qt 安装步骤 3.2 安装qt发现Ubuntu空间不足&#xff0c;怎么去扩容呢&#xff1f; 3.2.1 硬盘操作步骤&#xff08;需要关闭虚拟机进行操作&#xff09; 3.2.2 Ubuntu命令操作&#xff1a;安装…

基于单片机的模糊PID炉温控制系统设计

摘 要 电热炉是在工业热处理的生产中广泛使用的一种设备&#xff0c;电热炉的温度控制系统存在时变性&#xff0c;非线性&#xff0c;滞后性等特征&#xff0c;难以用常规PID的控制器对系统达到很好的控制效果。当控温精度的要求高时&#xff0c;使用传统的控制理论方法难以达…

蓝桥杯刷题|03普及-真题

[蓝桥杯 2017 省 B] k 倍区间 题目描述 给定一个长度为 N 的数列&#xff0c;​,,⋯&#xff0c;如果其中一段连续的子序列 ​,,⋯ (i≤j) 之和是 K 的倍数&#xff0c;我们就称这个区间 [i,j] 是 K 倍区间。 你能求出数列中总共有多少个 K 倍区间吗&#xff1f; 输入格式 …

微服务高级篇(一):微服务保护+Sentinel

文章目录 一、初识Sentinel1.1 雪崩问题及解决方案1.2 微服务保护技术对比1.3 Sentinel介绍与安装1.4 微服务整合Sentinel 二、Sentinel的流量控制三、Sentinel的隔离与降级四、Sentinel的授权规则五、规则持久化5.1 规则管理模式【原始模式、pull模式、push模式】5.2 实现push…

第二十六节 Java 重写(Override)与重载(Overload)

重写 (Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写&#xff01;返回值和形参都不能改变。即外壳不变&#xff0c;核心重写&#xff01; 重写的好处在于子类可以根据需要&#xff0c;定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。…

面试算法-48-二叉树的锯齿形层序遍历

题目 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,…

从Excel到山海鲸:我的数据可视化升级之旅

作为一名新用户&#xff0c;我最近有幸体验了山海鲸可视化软件&#xff0c;近期山海鲸可视化产品开放了可视化编辑全部功能&#xff0c;并支持本地化部署功能&#xff0c;在使用过程中它不仅打开了我对数据可视化全新世界的大门&#xff0c;而且在实际操作中为我带来了不少惊喜…

【C语言】数据在内存中的存储(包含大小端字节序问题)~

一、前言 我们在刚开始学习C语言的时候&#xff0c;就接触到了很多数据的不同类型。我们也知道&#xff0c;数据是存储在一块内存空间的&#xff0c;且我们只知道数据的类型决定着&#xff0c;该数据在内存中所占内存空间的大小&#xff0c;且超过一个字节的数据在内存中存储的…

【NLP笔记】Transformer

文章目录 基本架构EmbeddingEncoderself-attentionMulti-Attention残差连接LayerNorm DecoderMask&Cross Attention线性层&softmax损失函数 论文链接&#xff1a; Attention Is All You Need 参考文章&#xff1a; 【NLP】《Attention Is All You Need》的阅读笔记 一…

安科瑞智慧安全用电云平台【无人化数据监控 远程控制 运维管理】

背景 在住宅火灾中&#xff0c;电气引发的居高不下&#xff0c;已查明原因的火灾中有52%系电气原因引起&#xff0c;尤其是各类家用电器、电动车、电气线路等引发的火灾越来越突出&#xff0c;仅电动自行车引发的较大火灾就有7起。这些事故暴露出电器产品生产质量、流通销售&a…

引领展览新风尚:一站式搭建VR在线展馆,开启数字化展示新纪元

随着VR技术的不断成熟和普及&#xff0c;VR在线展馆已成为企业和用户展示展品的新颖方式。这种技术不仅能够提供沉浸式的观展体验&#xff0c;还能帮助企业和个人快速搭建属于自己的虚拟展馆。元居虚拟空间布展平台为用户提供了快速搭建VR在线展馆的便捷服务。 一、VR在线展馆的…

vue3+vite项目打包遇到的问题

一、项目打包出现空白页 vite.config.js中&#xff0c;添加base: ./ import { defineConfig } from vite import vue from vitejs/plugin-vueexport default defineConfig({base: ./, })router/index.js&#xff0c;将路由模式改成hash模式 import { createRouter, createWe…

4G/5G视频记录仪_联发科MTK6765平台智能记录仪方案

视频记录仪主板采用了联发科MT6765芯片&#xff0c;该芯片采用12nm FinFET制程工艺&#xff0c;8*Cortex-A53架构&#xff0c;搭载安卓11.0/13.0系统&#xff0c;主频最高达2.3GHz&#xff0c;待机功耗可低至5ma&#xff0c;并具有快速数据传输能力。配备了2.4英寸高清触摸显示…

YOLOv5独家改进:block改进 | RepViTBlock和C3进行结合实现二次创新 | CVPR2024清华RepViT

💡💡💡本文独家改进:CVPR2024 清华提出RepViT:轻量级新主干!从ViT角度重新审视移动CNN,RepViTBlock和C3进行结合实现二次创新 改进结构图如下: 收录 YOLOv5原创自研 https://blog.csdn.net/m0_63774211/category_12511931.html 💡💡💡全网独家首发创…