Linux——线程互斥与互斥锁的使用

news2025/1/24 11:48:27

目录

前言

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

二、互斥量(互斥锁)

三、互斥锁的使用

1.互斥锁的初始化

2.加锁与解锁

3.锁的使用

4.锁的封装

四、线程饥饿

五、互斥锁的原理

六、死锁


前言

我们学习过线程概念与线程控制,知道了线程的原理以及如何控制线程,由于线程可以创建多个,也就是操作系统中存在多个由某一个进程创建的执行流。那么他们在访问并修改共享资源时,可能会发生数据不一致的问题,进而我们需要让线程互斥来保护公共资源

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

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

二、互斥量(互斥锁)

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

学习了这么多概念,不如我们直接来上代码,看看为何需要进程进行互斥。

现在我们写一个模拟抢票的代码,总共只有10000张票,创建5个进程,让他们一起去访问调用这个抢票函数,访问ticket资源。

#include<iostream>
#include<thread>
#include<unistd.h>
#include<vector>
#include<functional>
using namespace std;


string Getname()
{
    static int num = 1;
    string name("thread_");
    name += to_string(num);
    num++;
    return name;
}

int ticket = 10000;

void _GetTicket(string name)
{
    while(1)
    {
        if(ticket>0)
        {
            usleep(1000);
            cout<<"我是: "<<name<<",剩余票数: "<<ticket<<endl;
            ticket--;
        }
        else
        {
            break;
        }
    }
}

void GetTicket()
{
    _GetTicket(Getname());
}

int main()
{
    int n = 5;
    vector<thread> threads;
    while(n--)
    {
        threads.push_back(thread(GetTicket));
    }
    for(auto& t : threads)
    {
        t.join();
    }
}

运行发现剩余票数竟然会发生负数,也就是抢票抢到了不存在的票,这肯定是不对的。 

我们代码中判断明明是ticket大于0,才能够继续抢票,为啥会发生这种情况?

 因为ticket是共享资源,当多线程访问同一共享资源时,我们需要加以互斥保护。

  • 如果不保护,那么多线程执行时,遇到的ticket共享资源,会进行抢票并对ticket一直--,当ticket被减到1时,此时某一线程进来了,发现ticket是1,就会进入内部继续访问。在抢票过程进行中(也就是if进入后,ticket--之前)
  • 多核情况下,其他进程也能在ticket是1时判断,并进入内部。
  • 单核情况下,时钟中断到来,进程会进行切换,其他进程依然能在ticket是1是判断,也进入内部
  • 因此进入了if判断内部,后续做--操作,又会重新从内存中读取数据,此时可能数据变为了0或者负数,就会让值减到负数。

由此,我们需要对共享资源进行保护,让线程互斥起来,也就是让资源变为临界资源——任何一个时刻,只允许一个线程正在访问公共资源 。我们把进程中访问临界资源的代码称之为临界区

此时我们发现,我们在对ticket的访问过程并不是原子性的——不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完成。因为访问过程可能随时被时钟中断,进程发生切换。

为了深刻理解原子性,我们写了如下这么简单的代码,我们知道C语言代码会被编译成汇编代码,再转化为二进制由CPU进行执行处理。那么一句简单的a++代码,就会有三句汇编代码。

先读取,再处理,再返回。 是有可能在第1步或第2部被中断。那并没有对内存进行修改,其他进程此时进来运行,查看到的内存数据还是1。那a++的操作,肯定不是原子性的。

那么什么样的操作是原子性的呢?

  • 要么是只需要一步操作就完成的
  • 要么就给进程加上互斥锁,就是只能让单个进程访问,再当前进程访问结束之前,其他进程无法进行访问。

三、互斥锁的使用

1.互斥锁的初始化

互斥锁的使用很简单,如下

定义全局锁:PTHREAD_MUTEX_INITIALIZER ,不需要销毁、

定义局部锁:pthread_mutex_init,需要销毁

  • 参数:mutex,pthread_mutex_t 是锁的类型,可以定义锁,然后传入&mutex即可。
  • 参数:atrr,定义锁的属性,nullptr代表默认属性

销毁锁:pthread_mutex_destory

  • 参数:mutex,传入&mutex即可。

2.加锁与解锁

定义了锁之后,还需要给代码加锁,加锁与解锁代码如下。

加锁阻塞:pthread_mutex_lock

加锁不阻塞:pthread_mutex_trylock

解锁:pthread_mutex_unlock

  • 参数都为锁的地址。

3.锁的使用

我们首先定义锁,然后初始化锁,对临界区进行加锁,临界区结束进行解锁。最后销毁锁。

并且我们需要尽可能的给少的代码加锁,因为有了锁之后,多线程就只有一个线程正在临界区中运行。效率并不高,如果给很多代码甚至全部代码加锁,那么创建多线程也就没有了意义。

那么现在,我们对之前的抢票代码做如下修改,定义全局锁并初始化,对访问ticket的代码进行加锁,if中的代码之前完前进行解锁。

注意因为ticket==0不会进入if判断,但是在此之前你锁已经加上了,所以else中也需要解锁,

加锁之后再运行,发现剩余票数不会再出现0以及负数了。因为同一时刻,只有一个进程在访问临界资源。

同时,申请锁一定是原子性的,要么申请失败,要么申请成功。当某一个进程申请锁成功后,再他没有释放锁之前,其他进程不可能申请成功。其他进程会在申请锁这里进行阻塞,也就是等待。

  • 如果使用的是pthread_mutex_trylock 。那么申请锁成功返回0继续执行,申请失败返回错误值也继续执行,需要用户自行去判断。
  • 也就是说 trylock 不会阻塞,用户通过if判断返回值为不为0来判断线程是否申请锁成功来进行代码编写。

虽然我们已经加锁,但是线程切换是不管你有没有持有锁,因此你会将你持有锁的信息一起保存在你的硬件上下文之中,等待其他线程调度运行,你再回来继续执行。因为你还没有解锁,你是持有锁的状态,那么其他线程无法获取到这把锁,也就会一直阻塞。

小总结:

  1. 申请锁是原子的,同一时刻只有一个线程申请成功。
  2. 使用 pthread_mute_lock 申请锁失败的线程会等待,pthread_mute_trylock会返回错误值。
  3. 持有锁的线程在访问临界区资源时,也会被切换,但是没关系,只要他没释放锁,其他线程也不会申请锁成功。

全局锁使用比较简单,定义锁使用即可。

如果是局部锁,需要从外部传参,因为要让进程去获取同一把锁,不然就没办法互斥。

4.锁的封装

每一次都需要加锁再解锁,是非常麻烦的事情,如果我们将加锁封装成一个类,构造的时候加锁,出了作用域自己会调用析构解锁,就会很方便。

如下,封装了一个LockGuard(守护者),构造函数加锁,析构函数解锁

#pragma once
#include <pthread.h>

// 不定义锁,外部会传递锁
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock)
        : _lock(lock)
    {
    }
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void UnLock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock)
        : _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.UnLock();
    }
private:
    Mutex _mutex;
};

那么我们现在使用就使用LockGurad构造一下就可以了。出了作用域会自动析构

四、线程饥饿

刚才的代码,我们在centos 7系统下(ubuntu可能结果不同),会发现thread_1一直能够运行,也就是该线程竞争锁的能力非常强,导致其他进程无法拥有锁。这是由于线程优先级不一致导致的问题。

解决饥饿问题只用互斥是不行的,要让线程执行的时候,具备一定的顺序性,这就是同步!!!(后续写完同步,链接会放在这里)

五、互斥锁的原理

我们一直再说申请互斥锁是原子的。这确实是必须的,必须保护好自己,才能更好的保护其他人。那么为何说他这个行为是原子的呢?

  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,该指令是原子的
  • mutex锁可以当做一个整形变量,只不过他还有其他数据,比如持有锁的线程是谁,等待队列等,我们姑且他是一个整形变量,1代表锁还未被使用,0代表锁已被使用

有了这两点知识,我们来看互斥锁的汇编伪码。

如下是执行过程

  1. 给al寄存器置0,此时锁的内容为1(还没有人申请锁) 
  2. 使用exchange指令让mutex锁和al寄存器中的数据进行交换,
  3. 判断寄存器里面的内容是否大于0来让线程做不同的处理,大于0证明申请到锁,返回执行后续代码,等于0证明没申请到锁,就在等待队列进行等待。

这里有好几句指令,为何说他是原子性的呢? 

如果线程① movb $0, %al 运行完被切换,保存好自己硬件上下文离开,线程②进来,执行到xchgb %al , mutex成功,那么锁就被线程②拿走了,进程①切换回来,加载自己的上下文继续执行,发现 xchgb %al , mutex 之后,当前 al 为0,于是被等待。

线程①xchgb %al , mutex成功,那么锁就被线程①拿走了,线程②也不会申请到锁,会被挂起等待。

也就是说,通过这样的代码,使得任何一个时刻,只有一个线程能够成功申请锁, 这样就保证了加锁的原子性。

解锁的时候,就将锁的数据置为1,呼叫其他进程从等待队列中出来继续争抢锁就可以了。

关于加锁的原则:谁加锁,谁解锁

六、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁其实并不常见,大部分情况下是复杂的代码,创建了很多锁,由于逻辑问题出现了互相申请锁的情况发生,才会造成死锁。

比如线程A,在持有锁1的情况下,想要申请锁2,线程B,在持有锁2的情况下,想要申请锁1。他们都不让步,都想等待对方将锁释放出来。于是都卡在申请锁的步骤里,发生了死锁。

只有一个锁也可能死锁,也就是持有锁的时候,想要再去申请这把锁,因为你还没有释放锁 ,mutex的值为0,于是一直申请不了,造成了死锁。

死锁的4个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配 

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

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

相关文章

积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)

文章目录 积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路&#xff08;优化前一万多导出失败&#xff0c;优化后支持百万级跨库表导出&#xff09;优化结果需求背景和解决方案的思考解决方案流程描述&#xff1a;关键代码引入easy excel新建…

约跑小程序源码(asp.net+vue+element++uniapp+sqlserver)

开发语言&#xff1a;c# 框架&#xff1a;后端 asp.net mvc pc管理页面&#xff1a;vueelement 数据库&#xff1a;sqlserver 开发软件&#xff1a;eclipse/myeclipse/idea 浏览器&#xff1a;谷歌浏览器 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X …

前端:SVG绘制流程图

效果 代码 html代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>SVG流程图示例</title><style>/* CSS 样式 */</style><script src"js/index.js"></script…

plasmo浏览器插件框架使用react和ant.design框架创建页面内容脚本UI样式注入

使用plasmo开发浏览器插件的时候&#xff0c;想要使用内容脚本UI注入自定义的UI组件&#xff0c;官方文档&#xff1a;Content Scripts UI – Plasmo&#xff0c;最好是搭配上好看的UI样式&#xff0c;所以可以集成ant.design的UI组件库&#xff0c;但是只集成组件还不行&#…

百度松果菁英班——机器学习实践四:文本词频分析

飞桨AI Studio星河社区-人工智能学习与实训社区 &#x1f96a;jieba分词词频统计 import jieba # jieba中文分词库 ​ with open(test.txt, r, encodingUTF-8) as novelFile:novel novelFile.read() # print(novel) stopwords [line.strip() for line in open(stop.txt, r,…

C语言--条件编译(常见的编译指令)

#if&#xff08;开始&#xff08;判断条件&#xff09;&#xff09;#endif&#xff08;结束&#xff09; 条件满足就参与编译&#xff0c;这里是一个判断的语句&#xff0c;当M大于0的时候&#xff0c;打印hehe不然就不打印 或者注释代码也好用 当#if 0的时候 &#xff0c;也…

SDWAN专线保护企业数据传输安全

企业数字化进程的加速和网络环境的复杂化&#xff0c;数据传输安全已经成为企业网络管理的头等大事。SD-WAN&#xff08;软件定义广域网&#xff09;作为一种新兴的网络技术&#xff0c;不仅能够提升网络性能和效率&#xff0c;还能够有效地保护企业数据传输的安全性。以下是SD…

亚马逊店铺引流:海外云手机的利用方法

在电商业务蓬勃发展的当下&#xff0c;亚马逊已经成为全球最大的电商平台之一&#xff0c;拥有庞大的用户群和交易量。在激烈的市场竞争中&#xff0c;如何有效地吸引流量成为亚马逊店铺经营者所关注的重点。海外云手机作为一项新兴技术工具&#xff0c;为亚马逊店铺的流量引导…

第六篇: 3.5 性能效果 (Performance)- IAB/MRC及《增强现实广告效果测量指南1.0》

​​​​​​​ 翻译计划 第一篇概述—IAB与MRC及《增强现实广告效果测量指南》之目录、适用范围及术语第二篇 广告效果测量定义和其他矩阵之- 3.1 广告印象&#xff08;AD Impression&#xff09;第三篇 广告效果测量定义和其他矩阵之- 3.2 可见性 &#xff08;Viewability…

ctfshow web入门 命令执行 web53--web77

web53 日常查看文件 怎么回事不让我看十八 弄了半天发现并不是很对劲&#xff0c;原来我发现他会先回显我输入的命令再进行命令的回显 ?cnl${IFS}flag.php||web54 绕过了很多东西 基本上没有什么命令可以用了但是 grep和?通配符还可以用 ?cgrep${IFS}ctfshow${IFS}???…

【论文速读】| 大语言模型平台安全:将系统评估框架应用于OpenAI的ChatGPT插件

本次分享论文为&#xff1a;LLM Platform Security: Applying a Systematic Evaluation Framework to OpenAI’s ChatGPT Plugins 基本信息 原文作者&#xff1a;Umar Iqbal, Tadayoshi Kohno, Franziska Roesner 作者单位&#xff1a;华盛顿大学圣路易斯分校&#xff0c;华盛…

PicGo + Gitee + VsCode - 搭建私人图床

文章目录 前言搭建图床VsCode 安装插件安装 PicGo准备 Gitee 图床测试 尾声 前言 本人是一个重度 vimer&#xff0c;并且喜欢客制化一些东西… Typora 固然好用&#xff0c;但不支持 vim…发现 vscode 中既可以使用 vim&#xff0c;也可以 md&#xff0c;用起来比较舒服.因此…

如何自定义项目启动时的图案

说明&#xff1a;有的项目启动时&#xff0c;会在控制台输出下面的图案。本文介绍Spring Boot项目如何自定义项目启动时的图案&#xff1b; 生成字符图案 首先&#xff0c;找到一张需要设置的图片&#xff0c;使用下面的代码&#xff0c;将图片转为字符文件&#xff1b; impo…

vscode 安装vim插件配置ctrl + c/v功能

搜索Vim插件 插件介绍部分有提示操作 首先安装该插件&#xff0c;然后按照下述步骤设置ctrl相关的快捷键&#xff0c;以便于脱离im快捷键而愉快的敲代码。 1.在“设置”搜索框内搜索vim.handleKeys&#xff0c;选择 Edit in settings.json 2. 设置ctrl-c,ctrl-v等快捷键置为fa…

【Frida】【Android】 10_爬虫之WebSocket协议分析

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

LDR6328助力Type-C普及,便捷充电,绿色生活更精彩

随着科技的进步和全球统一接口的需求&#xff0c;Type-C接口正日益受到青睐。越来越多的设备正选择采纳这一先进的接口设计&#xff0c;它的普及无疑在改善着我们的日常生活。 在过往&#xff0c;许多小功率设备如小风扇、蓝牙音箱、桌面台灯以及家用加湿器等&#xff0c;都普遍…

STC89C51学习笔记(五)

STC89C51学习笔记&#xff08;五&#xff09; 综述&#xff1a;文本讲述了代码中速写模板的创建、如何将矩阵键盘的按键与数字一一对应以及如何创建一个矩阵键盘密码锁。 一、速写模板 点击“templates”&#xff0c;再鼠标右键选择配置&#xff0c;按照以下方式即可修改一些…

Linux初学(十七)redis

一、简介 redis就是一个内存数据库 redis中的数据&#xff0c;都是保存在内存中 端口&#xff1a;6379 二、安装redis 方法一&#xff1a;编译安装 方法二&#xff1a;yum安装-epel 第一步&#xff1a;配置epel源 详见&#xff1a;http://t.csdnimg.cn/AFl1K第二步&#xff1a…

为什么苹果 Mac 电脑需要使用清理软件?

尽管 Apple Mac 电脑因其卓越的性能、简洁高效的 macOS 操作系统及独特的美学设计备受全球用户青睐&#xff0c;但任何电子设备在长期使用后都难以避免面临系统资源日渐累积的问题。其中一个重要维护需求在于&#xff0c;随着使用时间的增长&#xff0c;Mac电脑可能会由于系统垃…

【CicadaPlayer】demuxer_service的简单理解

G:\CDN\all_players\CicadaPlayer-github-0.44\mediaPlayer\SMPMessageControllerListener.cppplayer的demuxer服务类 std::unique_ptr<demuxer_service> mDemuxerService{nullptr};根据option (Cicada::options),可以决定音视频的不同操作,通过 hander可以获得具体使…