操作系统~Linux~线程的互斥,mutex互斥锁的使用及其原理

news2024/11/16 1:52:48

1.一些基本概念

        1.临界资源:凡是被线程共享访问的资源都是临界资源(多线程、多进程打印数据到显示器,显示器就是临界资源)

        2.临界区:代码中访问临界资源的代码(在代码中,不是所有的代码都是进行访问临界资源的。而访问临界资源的代码区域我们称之为临界区)

        3.对临界区进行保护的功能,本质就是对临界资源的保护。方式:互斥或者同步

        4.互斥:在任意时刻,只允许一个执行流访问某段代码(访问某部分资源〉,就可以称之为互斥

        5.同步:一般而言,让访问临界资源的过程在安全的前提下(一般都是互斥and原子的),让访问资源具有一定的顺序性(具有合理性,比如可以防止饥饿问题)


2.多线程不安全的情况

造成线程并不安全的因素有很多,这里讨论非原子性的情况,可以通过加锁来解决该问题

比如我们对一个变量进行--操作,a--并不是原子的

如果有一个线程要执行a--操作,需要经过下面三步

  • 1.load :将共享变量a从内存加载到寄存器中
  • 2.update : 更新寄存器里面的值,执行-1操作
  • 3.store :将新值,从寄存器写回共享变量a的内存地址

假设a的初始值是1000,线程A执行a--操作

        如果线程A执行完第二步,此时线程A时间片到了,线程A处理完的寄存器中的数据999保存在它自己的上下文中,然后在等待队列里面等待调度。

        此时线程B来了,把三步操作全部完成,让a减少了900,此时内存中的a被修改为100,然后线程A再继续完成最后一步操作,把999写回了内存中,此时内存中的a值又变成了999,这就导致了结果错误,也就是线程不安全的情况。


3.互斥锁pthread_mutex的使用

  • 定义锁

pthread_mutex_t mutex;

  • 初始化锁

方法一:静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:动态分配

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);

1.使用 PTHREAD_ MUTEX_ INITIALIZER静态分配初始化的互斥量不需要销毁

2.不要销毁一个已经加锁的互斥量

3.已经销毁的互斥量,要确保后面不会有线程再尝试加锁

  • 加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

  • 解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号

实现线程安全的抢票程序

主要是对上述几个方法的使用

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
const int N = 1000;

class Tickets
{
private:
    int tickets;  //当前票的数量
    pthread_mutex_t mtx;  //定义互斥锁mtx
    //这里可以将pthread_mutex互斥锁定义为成员变量,或者之后用到的地方定义为成员变量都可以
public:
    Tickets():tickets(N)
    {
        pthread_mutex_init(&mtx,nullptr);  //使用动态初始化的方式初始化互斥锁
    }

    ~Tickets()
    {
        pthread_mutex_destroy(&mtx);  //释放锁
    }

    bool getTicket()  //返回值为bool类型,后续让多个线程抢票,如果没有抢到票就需要停下来
    {
        bool res = true;
        //如果pthread_mutex互斥锁不定义为成员变量,可以在这里定义成静态局部变量
        pthread_mutex_lock(&mtx);  //加锁
        if(tickets > 0)
        {
            usleep(10000);  //休眠1000us,模拟抢票时间
            cout << "我的线程id是: " << pthread_self() << ",我抢到的票是第" << N-tickets+1 <<"张" << endl;
            tickets--;
        }
        else
        {
            cout << "我的线程id是: " << pthread_self() << ",我发现票已经被抢完了" << endl;
            res = false;
        }
        tickets--;
        pthread_mutex_unlock(&mtx);  //解锁

        return res;
    }
};

void *threadRoutine(void *args)
{
    pthread_detach(pthread_self());  //主线程中不关注新线程的返回值,所以可以detach分离线程
    Tickets *it = (Tickets*)args;  //注意需要将参数强转成Tickets*类型
    cout << "tests" << endl;
    while(true)
    {
        bool res = it->getTicket();
        if(res == false)
        {
            break;
        }
    }
}


int main()
{
    int n = 5;
    pthread_t tid[n];
    Tickets *ticket = new Tickets();  //ticket对象之后要作为参数传入线程执行函数中,所以这里定义为指针
    for(int i = 0; i < n; i++)
    {
        pthread_create(&tid[i],nullptr,threadRoutine,ticket);
    }
    pthread_exit(nullptr);  //新线程全部detach,主线程要先退出,否则会执行到return语句,从而进程被终止,其他新线程也就会被终止
    return 0;
}

4.互斥锁pthread_mutex的原理

该原理主要是为了证明mutex锁本身是线程安全的,也就是lock和unlock操作是原子的。

有一行代码底层操作的时候只有一行汇编的时候,这行代码就是原子的。

原理分析:

        1.mutex这个互斥量首先被保存在内存中,初始值是1,表示可以锁没有被占用。

        2.此时线程A和线程B竞争锁,线程A竞争成功,首先执行mov指令,将0值设置到寄存器中,此时寄存器中的数值就是线程A的上下文数据,就算mov完线程A被切走,回来的时候还是会恢复当前的上下文继续执行;

        3.第二步线程A执行xchgb指令,将寄存器中的值(0)和内存中mutex的值(1)进行交换,mutex互斥锁保证自己线程安全的核心就在这里,用一条汇编指令,将CPU寄存器中的内容与内存中的内容完成了交换。此时就算A被切走,其他线程获取内存中mutex的值的时候获取到的都是0,没有办法拿到锁,只能等待A解锁。

        4.第三步线程A判断寄存器中的内容是否>0,交换完以后,线程A相当于把获取到的互斥量(互斥锁)mutex保存在自己的上下文数据中了,也就是此时该寄存器中的内容,如果从内存中获取的mutex值>0,那就进入临界区,访问临界资源。

        5.如果线程A占用着锁,其他线程获取内存中mutex值的时候,会进入else逻辑,挂起等待,直到线程A解锁的时候,才会被唤醒,从而go to lock继续竞争锁。

        6.当线程A在执行临界区内的代码时,线程A可能会被切走,进入等待队列,此时内存中mutex值并不会发生变化。线程A被切走的时候,它的上下文数据也会被保护起来一起切走,此时锁的数据是保存在上下文中的,所以线程A相当于是抱着锁走的。在这个期间,其他线程不可能成功竞争到锁。

        7.站在其他线程的视角,线程A访问临界区就是原子性的了


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

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

相关文章

kotlin学习笔记之注解与反射

一、声明并应用注解 一个注解允许你把额外的元数据关联到一个声明上。然后元数据就可以被相关的源代码工具访问&#xff0c;通过编译好的类文件或是在运行时&#xff0c;取决于这个注解是如何配置的。 1、应用注解 在kotlin中使用注解的方法和java一样。要应用一个注解&#xf…

如何通过3个月自学成为网络安全工程师!

前言&#xff1a; 趁着今天下班&#xff0c;我花了几个小时整理了下&#xff0c;非常不易&#xff0c;希望大家可以点赞收藏支持一波&#xff0c;谢谢。 我的经历&#xff1a; 我 19 年毕业&#xff0c;大学专业是物联网工程&#xff0c;我相信很多人在象牙塔里都很迷茫&…

Pycharm配置关于pyside6的外部工具

文章目录一、前言二、Pycharm配置1、designer.exe&#xff08;1&#xff09;打开Pycharm的设置&#xff08;2&#xff09;相关参数&#xff08;可复制粘贴&#xff09;2、Pyside6-uic.exe&#xff08;1&#xff09;设置&#xff08;2&#xff09;相关参数&#xff08;可复制粘贴…

Java--抽象类和接口的区别

今天是22年最后一天了, 写篇博客记录一下吧, 这一年发生了很多事情, 也学到了很多知识, 后面要继续加油啊, 大家也要加油啊, 米娜桑. 目录 概述 区别 1. 定义关键字不同 2. 继承或实现的关键字不同 3. 子类扩展的数量不同 4. 属性访问控制符不同 5. 方法控制符不同 6.…

python互联网程序设计GUI程序设计和网络程序设计(人机互动聊天软件)

1&#xff0e;项目意义 1、了解网络的结构&#xff1b; 2、了解网络传输协议&#xff1b; 3、掌握基本的网络编程方法。 2&#xff0e;总体设计 使用 TCP 协议实现人机聊天互动&#xff0c;程序具有服务端和客户端&#xff1a; &#xff08;1&#xff09;必备功能&#xff1…

Java财务在线咨询网站系统财务咨询网

简介 财务咨询网站&#xff0c;可以咨询公司代办&#xff0c;代理记账等一系列的财务问题的资讯服务网站 演示视频 https://www.bilibili.com/video/BV1T54y1H7Ar/?share_sourcecopy_web&vd_sourceed0f04fbb713154db5cc611225d92156 角色 管理员客服注册用户游客 技术…

Spring之DI入门案例

目录 一&#xff1a;DI入门案例实现思路分析 1.要想实现依赖注入&#xff0c;必须要基于 IOC 管理 Bean 2.Service 中使用 new 形式创建的 Dao 对象是否保留 ? 3.Service 中需要的 Dao 对象如何进入到 Service 中 ? 4.Service 与 Dao 间的关系如何描述 ? 二&#xff1…

(Qt) cmake编译Qt项目

文章目录前言环境cmake基础预备的项目代码文件资源路径demo.promain.cppres.qrcmywidget.cppmywidget.hmywidget.ui运行效果CMake文件资源路径CMakeLists.txt生成与构建END前言 通常我们在编写qt的时候都是在Qt creator中。而如何在VS Code中编写qt就是本文需要解决的问题 环…

顺序表 —— 初始化、销毁、打印、增加、删除、查找、修改

1.何为线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串…线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直…

zotero导出pdf

今天老师给我改论文的时候布置了一个任务&#xff0c;让我把所有论文的pdf按格式打包发给她。可是之前我用zotero的时候都是在线保存的&#xff0c;有些是没有pdf的&#xff0c;怎么办&#xff1f;而且就算有pdf&#xff0c;他们的命名格式也五花八门&#xff0c;难道一个个手改…

kafka 消息日志原理 指定偏移量消费 指定时间戳消费

Kafka 日志详解 Apache Kafka日志存储在物理磁盘上各种数据的集合&#xff0c;日志按照topic分区进行文件组织&#xff0c;每一个分区日志由一个或者多个文件组成。生产者发送的消息被顺序追加到日志文件的末尾。 如上图所述&#xff0c;Kafka主题被划分为3个分区。在Kafka中&…

vscode使用跳板机(密钥)进入内网并连接内网中其它机器(密码)

经过简单测试 1、不能像xshell一样选择服务器的密钥登陆&#xff0c;只能通过将本机的公钥传到服务器上 2、不能使用本地socket5做代理登录 3、不能使用系统代理登录 一、使用密钥连接到跳板机 1、内网穿透 2、将本机公钥上传到服务器上 1&#xff09;建立密钥对 无论是win…

redis集群 mac安装

1.安装redis mac环境用brew install安装 brew install redis 安装好后默认配置启动单点服务 redis-server 注&#xff1a;brew默认程序安装在/usr/local/Cellar目录下 /usr/local/Cellar/redis 默认配置文件在 /usr/local/etc/redis.conf 2.创建配置文件 准备创建6个节…

谣言检测数据集

1 PHEME-R 这是一个在PHEME FP7项目的新闻学用例中收集和注释的数据集。这些谣言与9个不同的突发新闻相关。它是为分析社交媒体谣言而创建的&#xff0c;包含由谣言推文发起的推特对话&#xff1b;对话包括对这些谣言推文的回应推文。这些推文已经被注解为支持度、确定性和证…

VS2012安装教程

我要学只有我们两个人懂得C语言。 安装包&#xff1a;https://pan.baidu.com/s/1YR7Xk9Zlv7zQWCsERdVgIQ [提取码]&#xff1a;stvi 1、右键以管理员身份运行 “vs_ultimate.exe” 2、编辑软件安装位置&#xff0c;然后点击同意许可&#xff0c;之后点下一步即可&#xff01; 3…

mongoDB聚合查询

管道 管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。 聚合管道操作 可参考菜鸟文档&#xff1a;菜鸟文档 命令 功能描述 $project指定输出…

shell第四天作业——流程控制之循环

题目 一、for创建20个用户&#xff0c;用户前缀由用户输入&#xff0c;用户初始密码由用户输入。 二、for循环ping测试指定网段的主机&#xff0c;网段由用户输入。 三、使用for/while实现批量主机root密码的修改 一、for创建20个用户&#xff0c;用户前缀由用户输入&#x…

2022年已然要结束了,一起来分享下你的故事吧!2023年的接力棒已经递到手里,千言万语不如一句Fighting!

【系列专栏】&#xff1a;博主结合工作实践输出的&#xff0c;解决实际问题的专栏&#xff0c;朋友们看过来&#xff01; 《QT开发实战》 《嵌入式通用开发实战》 《从0到1学习嵌入式Linux开发》 《Android开发实战》 《实用硬件方案设计》 长期持续带来更多案例与技术文章分享…

c++语法欠缺地方

sizeof是用来计算变量占多大内存的&#xff0c;单位是字节&#xff08;byte&#xff09;&#xff1b;sizeof 后面跟类型时&#xff0c;必须加上括号&#xff0c;例如sizeof(double);后面跟变量可以不用加括号&#xff0c;例如&#xff1a;sizeof d%d是以十进制形式输出有符号整…

CDP集群卸载过程

CDP集群卸载过程 1. 登录到Cloudera Manager&#xff0c;并停止整个集群服务。 2. 停用并移除所有Parcel 3. “停用”CDH7的Parcel。 4. 从主机删除”CDH6的Parcel 5. 删除集群 6. 登录server机器&#xff0c;停止CM Server服务 systemctl stop cloudera-scm-server 7. 移除…