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

目录

前言

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

二、互斥量(互斥锁)

三、互斥锁的使用

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个必要条件

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

避免死锁

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

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

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

相关文章

Django项目定时任务django-crontab

首先定义一个定时任务函数tasks.py&#xff08;见文章末尾示例&#xff09;&#xff0c;编写函数&#xff0c;然后在setting.py中配置定时任务 1、首先安装django-crontab pip install django-crontab 2、在setting.py中添加应用 (在所有自定义注册app之上) INSTALLED_APPS …

第四百四十六回

文章目录 1. 概念介绍2. 使用方法3. 示例代码4. 经验与总结4.1 经验分享4.2 内容总结 我们在上一章回中介绍了"overlay_tooltip简介"相关的内容&#xff0c;本章回中将再谈flutter_launcher_icons包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

Advanced RAG 02:揭开 PDF 文档解析的神秘面纱

编者按&#xff1a; 自 2023 年以来&#xff0c;RAG 已成为基于 LLM 的人工智能系统中应用最为广泛的架构之一。由于诸多产品的关键功能&#xff08;如&#xff1a;领域智能问答、知识库构建等&#xff09;严重依赖RAG&#xff0c;优化其性能、提高检索效率和准确性迫在眉睫&am…

Android详细介绍POI进行Word操作(小白可进)

poi-tl是一个基于Apache POI的Word模板引擎&#xff0c;也是一个免费开源的Java类库&#xff0c;你可以非常方便的加入到你的项目中&#xff0c;并且拥有着让人喜悦的特性。 一、使用poi前准备 1.导入依赖&#xff1a; 亲手测过下面Android导入POI依赖的方法可用 放入这个 …

如何成为一名优秀的工程师下

身为工程师&#xff0c;理所当然要重视实践&#xff0c;自然科学不管发展到何时都离不开实验。 电子学本身就是 为了指导工程实践。所以不要谈空洞的理论。现在很多毕业生都面临这样的问题&#xff0c;总是谈一些空洞的理论&#xff0c;甚至错误的但还不以为然的理论。实践可以…

anylabeling使用和安装

源码地址&#xff1a; git clone https://github.com/vietanhdev/anylabeling.git Auto Labeling with Segment Anything Youtube Demo: https://www.youtube.com/watch?v5qVJiYNX5KkDocumentation: https://anylabeling.nrl.ai Features: Image annotation for polygon, r…

个性化硬盘显示图标的教程

前言 前段时间有个小伙伴说想要个性化硬盘显示的图标&#xff0c;让电脑看起来更加高级。 这看起来确实很高级&#xff0c;这种图标是怎么设置的呢&#xff1f;今天小白就来唠唠&#xff5e; 更换图标的步骤其实并不难&#xff0c;主要的步骤为&#xff1a; 找一个好看的图标…

算法设计与分析(实验5)-----图论—桥问题

一&#xff0e;实验目的 掌握图的连通性。掌握并查集的基本原理和应用。 二&#xff0e;实验步骤与结果 1.定义 &#xff08;1&#xff09;图的相关定义 图&#xff1a;由顶点的有穷非空集合和顶点之间的边的集合组成。 连通图&#xff1a;在无向图G中&#xff0c;若对于…

idea链接gitlab的token到期

报错 HTTP Request Request GET http://36.46.143.158:6060/api/v4/version failed wit

全坚固笔记本丨工业笔记本丨三防笔记本相较于普通笔记本有哪些优势?

三防笔记本和普通笔记本在设计和性能方面存在显著差异&#xff0c;三防笔记本相较于普通笔记本具备以下优势&#xff1a; 三防笔记本通常采用耐磨、耐摔的材料&#xff0c;并具有坚固的外壳设计&#xff0c;能够承受恶劣环境和意外碰撞&#xff0c;有效保护内部组件不受损坏。相…

Flutter第六弹 基础列表ListView

目标&#xff1a; 1&#xff09;Flutter有哪些常用的列表组建 2&#xff09;怎么定制列表项Item&#xff1f; 一、ListView简介 使用标准的 ListView 构造方法非常适合只有少量数据的列表。我们还将使用内置的 ListTile widget 来给我们的条目提供可视化结构。ListView支持…

10倍提效!用ChatGPT编写系统功能文档。。。

系统功能文档是一种描述软件系统功能和操作方式的文档。它让开发团队、测试人员、项目管理者、客户和最终用户对系统行为有清晰、全面的了解。 通过ChatGPT&#xff0c;我们能让编写系统功能文档的效率提升10倍以上。 ​《Leetcode算法刷题宝典》一位阿里P8大佬总结的刷题笔记…

Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks

原文链接&#xff1a;https://arxiv.org/abs/1908.10084 提出契机&#xff1a; 提升相似文本的检索速度 在自然语言处理领域&#xff0c;BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;和RoBERTa&#xff08;A Robustly Optimized B…

mysql修改密码提示: Your password does not satisfy the current policy requirements

1、问题概述&#xff1f; 环境说明&#xff1a; Red Hat Enterprise Linux7mysql5.7.10 执行如下语句报错&#xff1a; set password for rootlocalhost password(123456); ERROR 1819 (HY000): Your password does not satisfy the current policy requirements意思就是&a…

深度学习之使用BP神经网络识别MNIST数据集

目录 补充知识点 torch.nn.LogSoftmax() torchvision.transforms transforms.Compose transforms.ToTensor transforms.Normalize(mean, std) torchvision.datasets MNIST&#xff08;手写数字数据集&#xff09; torch.utils.data.DataLoader torch.nn.NLLLoss() to…

Vue 有哪些主要的指令修饰符

目录 1. 什么是指令修饰符 2. 指令修饰符有哪些 2.1. 按键修饰符 2.2. v-model修饰符 2.3. 事件修饰符 1. 什么是指令修饰符 通过 "." 指明一些指令 后缀&#xff0c;不同 后缀 封装了不同的处理操作 目的&#xff1a;简化代码 2. 指令修饰符有哪些 2.1. 按键…

SpringMVC数据响应和请求

文章目录 1.SpringMVC简介2. SpringMVC快速入门3. SpringMVC执行的流程4.SpringMVC注解解释5. 视图解析器6.SpringMVC的数据响应6.1返回ModelView对象6.2直接返回字符串6.3返回json字符串 7.SpringMVC获得请求数据7.1 获得基本类型参数7.2获得POJO类型参数7.3获取数组类型参数7…

Python | Leetcode Python题解之第15题三数之和

题目&#xff1a; 题解&#xff1a; class Solution:def threeSum(self, nums: List[int]) -> List[List[int]]:n len(nums)nums.sort()ans list()# 枚举 afor first in range(n):# 需要和上一次枚举的数不相同if first > 0 and nums[first] nums[first - 1]:continu…

【问题处理】银河麒麟操作系统实例分享,银河麒麟高级服务器操作系统mellanox 网卡驱动编译

1.Mellanox 网卡源码驱动下载链接&#xff1a; https://www.mellanox.com/downloads/ofed/MLNX_EN-5.7-1.0.2.0/MLNX_EN_SRC-5.7-1.0.2.0.tgz 2.系统及内核版本如下截图&#xff1a; 3.未升级前 mellanox 网卡驱动版本如下&#xff1a; 4.解压 “MLNX_EN_SRC-5.7-1.0.2.0.tg…

uniapp使用npm命令引入font-awesome图标库最新版本

uniapp使用npm命令引入font-awesome图标库最新版本 图标库网址&#xff1a;https://fontawesome.com/search?qtools&or 命令行&#xff1a; 引入 npm i fortawesome/fontawesome-free 查看版本 npm list fortawesome在main.js文件中&#xff1a; import fortawesome/fo…