【Linux】线程安全问题①——如何实现资源访问互斥(附图解与代码实现)

news2024/11/25 4:35:47

线程安全主要分为两个方面,分别是资源访问互斥与线程同步(线程协同配合)

本篇博客,我们主要来讲解资源访问互斥这一方面

目录

为什么要实现资源访问互斥?

实现资源访问互斥(原子访问)的经典机制——互斥锁

互斥锁相关函数

使用互斥锁实现资源访问互斥的具体实现

代码实现

结果图示


为什么要实现资源访问互斥?

我们来做一个情景假设

假设现在有两个线程,分别是线程A和线程B,有一个全局变量,名为num,num的初始值位0。

现在这两个线程都要对num这个全局变量进行+1操作,并且都得到了时间片,大家觉得结果一定是2吗?

还真不一定,为什么呢?我来通过几条汇编指令来给大家讲解一下

首先我们要知道 " num = num + 1 ; " 这条语句在计算机中的汇编语言是如何实现的

  1. mov eax , num #将num的数值放入寄存器eax中
  2. add eax  , 1 #将寄存器eax中的数值+1
  3. mov num , eax #再将eax中的数值赋给num

假设线程A和线程B同时获得了num的初始值,也就是他们的第一条汇编指令是同时进行的,不论后续两步谁快谁慢,num最后的结果都是1,因为他们寄存器中num的数值都是0。只有两个线程先后进行对num数值的修改,才能够得到正确的结果2

为什么会出现这种情况呢?就是因为他们没实现对资源的原子访问,两个线程互相将对方的结果覆盖了。所以我们要采用方法杜绝这种情况,实现资源访问互斥,让两个线程对这些共享数据实现原子访问。也就是当自己对这些共享数据进行读写的时候,其余线程不可以对这些数据进行读写操作

实现资源访问互斥(原子访问)的经典机制——互斥锁

实现原子访问的经典机制之一,便是互斥锁机制。要理解互斥锁机制,我们可以拿身边最常见的一个东西来进行举例,那就是卫生间

我做一个情景假设,来帮助大家理解互斥锁机制

假设现在陆续有十个人要来上厕所,卫生间只有一个

  1. 第一个人到了,尝试开卫生间的门(其实也就是尝试获取卫生间门锁的使用权),发现门没有被锁上,于是获得了卫生间门锁的使用权,锁上了卫生间的门并使用卫生间
  2. 第二个人到了,尝试开卫生间的门,发现门被锁上了,于是排在第一个等待卫生间开门
  3. 其他的人到了,尝试开卫生间的门,发现门被锁上了,排在第一个人后面等待卫生间开门
  4. 第一个人出来了,对卫生间的门进行解锁,卫生间现在可供一个人使用
  5. 排在队伍的第一个人获得了卫生间门锁的使用权,锁上了卫生间的门并使用卫生间,后面的步骤同上

卫生间就好比数据,卫生间的门锁就好比互斥锁。

每个线程要想使用这些共享数据,就要先尝试获取互斥锁,如果发现卫生间门锁被锁上,也就是该互斥锁正在被使用的话,这些线程就会进入资源等待队列,等待这个锁的使用权,等到该互斥锁可以使用了,排在最前面的线程就会得到该锁,并对需要访问的共享资源进行上锁,从而正确使用这些共享资源

PS:

  1. 相同共享资源的互斥锁只能有一把,如果有多把互斥锁,每个线程都可以拿着互斥锁对共享资源进行上锁的话,互斥锁的存在就没有意义了
  2. 线程间的共享资源包括:全局资源(全局变量就属于全局资源的一种)、文件描述符、进程信息、堆区空间、信号行为、库空间等
  3. 在互斥锁保护区间的代码也被称为临界区代码,临界区代码越简短越好,否则会影响工作效率
  4. 拿到互斥锁的线程在解锁后可能会再次拿到互斥锁,不是一个线程只能拿一次

如果还是不能够理解的话,我们仍旧拿上面的那两个线程举例,并和上面的情景进行比对

假设现在有两个线程,分别是线程A和线程B,要对初始值位0的全局变量num进行加一操作,步骤如下

步骤卫生间情景线程情景
第一个人到了,尝试获取卫生间门锁的使用权,发现门没有被锁上,于是获得了卫生间门锁的使用权,锁上了卫生间的门并使用卫生间线程A到了,尝试获取互斥锁,发现锁没有被使用,于是获得了该锁的使用权,并对全局变量num进行上锁操作
第二个人到了,尝试开卫生间的门,发现门被锁上了,于是排在第一个等待卫生间开门线程B到了,尝试获取互斥锁,但是锁只有一把,于是便进入资源等待队列等待该锁的使用权
第一个人出来了,对卫生间的门进行解锁,卫生间门锁现在可供一个人使用线程A对全局变量num的+1操作完成,对该互斥锁进行解锁操作,锁现在可以被一个线程使用
第二个人获得了卫生间门锁的使用权,锁上了卫生间的门并使用卫生间线程B获得了该互斥锁的使用权,并对全局变量num进行上锁操作
第二个人出来了,对卫生间的门进行解锁,卫生间被使用了两次, 情景完成线程B对全局变量num的+1操作完成,对该互斥锁进行解锁操作,num的结果为2,情景完成

以上大致就是实现互斥锁机制的具体过程了,接下来我们来了解以下相关函数

互斥锁相关函数

这里我们要用到一个结构体,叫做pthread_mutex_t,这是互斥锁的相关结构体

先介绍下一会会用到的几个变量:

  • pthread_mutex_t lock ; //定义一个互斥锁的结构体
  • const pthread_mutexattr_t attr ; //锁属性相关结构体,使用默认属性就直接传NULL
函数功能返回值
pthread_mutex_init(&lock , &attr);实现互斥锁的初始化成功完成之后会返回0,其他任何返回值都表示出现了错误
pthread_mutex_destroy(&lock);释放锁资源所占用的内存成功返回0,失败返回错误编号
pthread_mutex_lock(&lock);上锁成功返回0,失败返回错误编号
pthread_mutex_unlock(&lock);解锁成功返回0,失败返回错误编号

使用互斥锁实现资源访问互斥的具体实现

在了解了相关函数和实现资源互斥访问的情况下,我们来写一段小代码,来实现一个功能:

  • 两个线程各对全局变量num加5000次,每次加1 (难度:⭐⭐)

PS:要注意的是,不是每个线程一次加满5000次才让另一个线程再加5000次,而是两个线程轮流加,最后都能加满5000次

代码实现

//mutex_lock.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>

//将次数定义成宏
#define CONT 5000

//将互斥锁与num定义成全局变量,来让两个线程都可以获取该锁和num
pthread_mutex_t lock;
int num = 0;

//两个线程的工作是一样的,所以这里只用定义一个工作函数
void* thread_job(void* arg)
{
    int i = 0;
    pthread_detach(pthread_self());//将线程设置为分离态,让系统自己回收
    //对num进行循环+1操作
    //注意,不要把锁加在循环外面,如果放在外面,就代表着让一个线程一次性加满5000次后再让另一个线程加
    //如果把锁家在循环外面,两个线程的工作效率还不如单线程工作效率高
    while(i < CONT)
    {
        pthread_mutex_lock(&lock);//上锁
        //这就是临界区代码,在上锁与解锁之间的代码就是临界区代码
        num++; 
        i++;
        printf("thread No.0x%x ++num , num = %d\n" , (unsigned int)pthread_self() , num);
        pthread_mutex_unlock(&lock);//解锁
    }
    pthread_exit(NULL);
}

int main()
{
    //初始化互斥锁
    pthread_mutex_init(&lock , NULL);
    pthread_t tids[2];
    //创造线程A和线程B
    pthread_create(&tids[0] , NULL , thread_job , NULL);
    pthread_create(&tids[1] , NULL , thread_job , NULL);
    //让主线程循环睡眠,来让线程A和线程B获取时间片
    //由于我们把线程设置成了分离态,系统会自动回收线程,不用我们操心
    while(1)
    {
        sleep(1);
    }
    //回收锁资源
    pthread_mutex_destroy(&lock);
    exit(0);
}

结果图示

我们可以发现,以上代码实现了该功能并完成了我们的要求,没有让一个线程一次性加满五千次,在加到9310的时候就发生了一次线程转换

以上就是本篇博客的全部内容了,大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答

今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!

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

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

相关文章

JMeter连接数据库

一. 下载数据库驱动jar包 https://jdbc.postgresql.org/download/ 二. 将数据库驱动放到jmeter的lib目录下 三. 在jmeter中引用这个jar包 四. 添加一个jdbc数据库连接配置 五. 添加一个jdbc request来查询sql并将查询结果赋值给一个变量 六. 将查询结果用于其他请求

fico入门基础

Fico模块 会计主体一般为公司法人 分公司不算一个会计主体 分公司上金融中心 子公司会算一个会计主体 子公司上公司代码 会计期间:就是会计会一个期间结算一次(一般为一个月结算一次)(不同国家的快递期间起点会有不同;如日本四月份为第一个快递期间,三月份为第十二个快递期…

OTN较WDM系统的 升级功能

文章目录 1、增加运维规则&#xff0c;具体措施是增加了帧结构&#xff0c;提升业务的监控管理运维能力。2、增加电交叉功能&#xff0c;使得OTN系统可以分别处理客户业务信号和波分信号。 OTN较WDM系统的 升级功能主要表现在下面两方面&#xff1a; 1、增加运维规则&#xff0…

驾校倒闭了这么多,凭什么我能活下来,因为有云表利剑

因为部门场地比较分散&#xff0c;学员名单都是通过wps传递&#xff0c;但是某个部门更新学员信息时&#xff0c;其他部门无法实时共享更新&#xff0c;导致其他部门无法掌握学员最新信息&#xff0c;为学员提供制定服务。用了云表后学员只需在一个部门办一次业务&#xff0c;其…

专题三:穷举、暴搜、深搜、回溯、剪枝【递归、搜索、回溯】

1、全排列 class Solution { public:vector<vector<int>> ret;vector<int> path;bool check[7];void dfs(vector<int>& nums){if(nums.size() path.size()) {ret.push_back(path);return;}for(int i 0;i < nums.size();i){if(check[i] fals…

2023 年值得关注的软件测试趋势

随着软件在商业和日常生活中继续发挥关键作用&#xff0c;测试将不断发展以满足现代数字环境的需求。因此&#xff0c;组织努力更快、更频繁地交付他们的软件&#xff0c;并且测试过程需要更多地集成到开发过程中。 到2023年&#xff0c;我们可以看到各种软件测试趋势的出现&am…

轻游戏风格虚拟资源付费下载模板Discuz论坛模板

轻游戏风格虚拟资源付费下载模板Discuz论坛模板&#xff0c;游戏资讯付费VIP源码模板。 模板说明&#xff1a; 1、模板名称&#xff1a;"qing游戏风格"&#xff0c;版本支持&#xff1a;discuzx3.0版本&#xff0c;discuzx3.1版本&#xff0c;discuzx3.2版本&#…

4.查询用户的累计消费金额及VIP等级

思路分析&#xff1a; &#xff08;1&#xff09;按照user_id及create_date 分组求消费金额total_amount &#xff08;2&#xff09;开窗计算同user_id下的累计销售金额sum(total_amount) over(partition by user_id order by create_date ROWS BETWEEN UNBOUNDED PRECEDING AN…

报错——warning: ignoring JAVA_HOME=/home/jdk/jdk1.8.0_281; using bundled JDK

我使用了es的8.3.0版本&#xff0c;但es从7.17版本以后不再支持jdk1.8了&#xff0c;需要进行JDK的版本升级&#xff0c;或者降低es的版本。 es和jdk对比版本

口袋参谋:如何写出高权重标题?用对这招很重要!

​如何写出高权重标题&#xff1f;这是99.99%的卖家都存在的疑虑&#xff01; 以前写高权重标题&#xff0c;很多卖家往往会复制同行竞品爆款标题到淘宝首页搜索框&#xff0c;然后在全标题后面加上几个字母&#xff0c;就可以拆分爆款标题。 这个问题我之前也说过&#xff0…

【Java基础面试三】、说一说你对Java访问权限的了解

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;说一说你对Java访问权限…

css3自动吸附scroll-snap

我们希望可以一块一块的滚动&#xff0c;比如当前一个块滚出去了一部分并且后一个块滚进来一部分的时候&#xff0c;实现后一个块自动滚入或者前一个块回弹到初始位置这种效果&#xff0c;以前的时候用js需要写比较复杂的判断逻辑&#xff0c;后来有了一个css scroll snap这个方…

Kotlin笔记(四):高阶函数

1. 高阶函数 1.1 定义高阶函数 高阶函数和Lambda的关系是密不可分的。一些与集合相关的函数式API的用法&#xff0c;如map、filter函数等,Kotlin的标准函数&#xff0c;如run、apply函数等。这几个函数有一个共同的特点&#xff1a;它们都会要求我们传入一个Lambda表达式作为参…

从虚拟电厂在上海的实践探索看企业微电网数字化的意义

安科瑞 华楠 作为典型的人口聚集、负荷密集区域&#xff0c;上海市具有外来电比例高、本地资源禀赋不足的特点。从发电侧角度来看&#xff0c;近年来上海风、光等新能源发电装机比例逐年提升&#xff0c;传统的火电逐渐成为调节性发电资源&#xff1b;从负荷侧角度来看上海以第…

什么是补丁管理,如何选择补丁管理软件

补丁管理是识别、测试、部署和安装软件补丁&#xff08;或更新&#xff09;到计算机的过程。软件补丁是一段代码&#xff0c;专为修复软件中的现有错误/漏洞、添加新功能或增强其安全性而量身定制。 通常&#xff0c;软件补丁管理过程包括扫描网络中的计算机以查找缺失的补丁&…

WATLOW CAS200 CLS216 释放人工智能(AI)能力用于导航

WATLOW CAS200 CLS216 释放人工智能(AI)能力用于导航 正如本周在自动化2019展示和会议&#xff0c;MiR1000可以在动态环境中自动拾取、运输和交付托盘和其他重达1000千克(2200磅)的重物。与2018年推出的MiR500一样&#xff0c;MiR1000旨在作为工厂车间叉车的协作、安全和灵活…

架构-设计原则

1、面向对象的SOLID 1.1 概述 SOLID是5个设计原则开头字母的缩写&#xff0c;其本身就有“稳定的”的意思&#xff0c;寓意是“遵从SOLID原则可以建立稳定、灵活、健壮的系统”。5个原则分别如下&#xff1a; Single Responsibility Principle&#xff08;SRP&#xff09;&am…

c++香甜的黄油(acwing)

农夫John发现了做出全威斯康辛州最甜的黄油的方法&#xff1a;糖。 把糖放在一片牧场上&#xff0c;他知道 N 只奶牛会过来舔它&#xff0c;这样就能做出能卖好价钱的超甜黄油。 当然&#xff0c;他将付出额外的费用在奶牛上。 农夫John很狡猾&#xff0c;就像以前的巴甫洛夫…

双十一购物指南:电视盒子哪个牌子好?口碑电视盒子品牌排行榜

双十一可以说是年度最低价&#xff0c;我们都会在此时买买买&#xff0c;电视盒子作为日常必备销量同样火爆&#xff0c;近来很多朋友咨询小编电视盒子哪个牌子好&#xff0c;因此我整理了用户评价最高的电视盒子品牌排行榜&#xff0c;看完就知道哪些电视盒子最值得买了。 推荐…