当前位置: 首页 > news >正文

Linux之线程互斥

目录

一、问题引入

二、线程互斥

1、相关概念

2、加锁保护

1、静态分配

2、动态分配

3、锁的原理

4、死锁

三、可重入与线程安全

1、概念

2、常见的线程不安全的情况

3、常见的线程安全的情况

4、常见不可重入的情况

5、常见可重入的情况

6、可重入与线程安全联系

7、可重入与线程安全区别


一、问题引入

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

我们来看看下面的多线程抢票系统的代码:

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <pthread.h>using namespace std;int ticket = 100;void *getticket(void *arg)
{char *name = (char *)arg;while (true){if (ticket > 0){usleep(1000);cout << name << ":"<< " " << ticket << endl;ticket--;}elsebreak;}
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}

这里的ticket变量是一个全局变量,那么它就会被所有线程共享。创建线程后,所有线程访问getticket函数,对其进行了重入,访问ticket并对ticket--。但是,我们发现,票数出现了负数,这完全不符合我们的代码逻辑和想要的结果。这是为什么呢?

首先,程序在编译的时候会被编译成汇编代码, 而在汇编代码中,ticket--操作在我们看来只有一行代码,但是在汇编中它其实分为了三步:1、将ticket值拷入到CPU寄存器中;2、CPU对其进行--操作;3、将结果写回内存。

而我们知道进程是有时间片的,在执行完上面任意一步时,线程可能因为时间片到了而被切换。而这就会造成一些问题。如下图:

线程A先进入,在完成第二步 -- 操作后,因为时间片到了,要被切换出去,99作为上下文数据被保存起来随A一起被切换。线程B进入,因为B的时间片比较长,他把ticket值减到了50并写回了内存后,时间片到了,被切换。线程A再次进入CPU,把上下文恢复,然后接着第3步执行,直接把99写到了内存里面。

线程B明明已经让ticket的值减到了50,结果你个线程A又直接把结果改成了99。这样就出现了数据错乱的现象。

在我们对ticket进行并发访问的时候,由于ticket- - 操作并不是原子的,所以出现了数据不一致的情况。这种情况怎么解决呢?我们接着往下讲。

二、线程互斥

1、相关概念

1、临界资源:多线程执行流共享的资源就叫做临界资源。
2、临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
3、互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
4、原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

2、加锁保护

为了解决上面代码的数据不一致的问题,需要做到三点:

1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

而其中最简单的一种方法就是对临界资源进行加锁保护。以达到下面的效果:

定义和初始化锁的函数: 

NAMEpthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutexSYNOPSIS#include <pthread.h>1、int pthread_mutex_destroy(pthread_mutex_t *mutex);2、int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);3、pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t 是由原生线程库给用户提供的一个数据类型,就是我们常说的锁。上图的 1和2 是对锁进行局部定义时的销毁和初始化操作,相当于析构函数和构造函数。

上图的 3 是对全局锁或者static静态锁进行初始化的方式。下面我们一一讲解。

加锁和解锁函数:

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请锁,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁,再去申请锁。

NAMEpthread_mutex_lock,  pthread_mutex_trylock,  pthread_mutex_unlock  -  lock   andunlock a mutexSYNOPSIS#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);

1、静态分配

静态分配就是我们 3 对应的对锁定义和初始化的方式。我们使用它对抢票代码进行保护。

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 100;void *getticket(void *arg)
{char *name = (char *)arg;while (true){pthread_mutex_lock(&mutex); // 加锁保护,其他线程只能在这阻塞等待,直到拿到锁if (ticket > 0)             // 这部分代码只能串行执行{usleep(rand() % 10000);cout << name << ":"<< " " << ticket << endl;ticket--;pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁,// 让其他线程能够拿锁访问}else{pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁// 让其他线程能够拿锁访问break;}usleep(rand() % 2000000);}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid() ^ 433);pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}

注:加锁的时候,一定要保证加锁粒度越小越好。最好不要让一些非临界区也被加锁保护。

2、动态分配

如果我们定义的锁是一个局部变量,那么我们就要像下面的代码这样使用锁:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>using namespace std;
#define THREAD_NUM 5class threaddata
{
public:threaddata(const string &s, pthread_mutex_t *m): name(s), mtx(m){}public:string name;pthread_mutex_t *mtx;
};int ticket = 100;void *getticket(void *arg)
{threaddata *td = (threaddata *)arg;while (true){pthread_mutex_lock(td->mtx);if (ticket > 0)              {usleep(rand() % 10000);cout << td->name << ":"<< " " << ticket << endl;ticket--;pthread_mutex_unlock(td->mtx);}else{pthread_mutex_unlock(td->mtx);break;}usleep(rand() % 2000000);}delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);srand((unsigned long)time(nullptr) ^ getpid() ^ 433);pthread_t t[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);threaddata *td = new threaddata(name, &mtx);pthread_create(t + i, nullptr, getticket, (void *)td);}for (int i = 0; i < THREAD_NUM; i++)pthread_join(t[i], nullptr);pthread_mutex_destroy(&mtx);return 0;
}

3、锁的原理

通过加锁,我们能够保证执行临界资源的操作是原子的。可是,访问临界资源时,多个线程要申请同一把锁,那么就必须要能够看到同一把锁,那么这个锁不就成了一个临界资源了吗,那锁是怎么保证自己的安全的呢?

为了保证锁的安全,申请和释放锁的操作也必须是原子的。如何保证呢?

在汇编的角度,如果只有一行汇编语句,我们就认为该汇编语句的执行是原子的。一般来说,是使用swap或exchange指令,以一条汇编语句,将内存和CPU寄存器的数据进行交换。如下图:

线程a是第一个申请锁的。它先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 1,mutex为0。接着,判断%al的内容 >0,返回,成功拿到锁。线程a切出,寄存器%al的数据作为上下文随线程a一起切出。(当然,线程a可能在任何时候被切出,这是线程a时间片比较长的情况)。

线程b,接着申请锁。 它也先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 0,mutex为0。接着,判断%al的内容不大于0,于是线程b挂起等待。只有线程a将锁释放后,才能重新申请锁。

4、死锁

死锁:多线程场景中, 多个执行流彼此申请对方的锁资源,并且还不释放自己已申请的锁资源,进而导致执行流无法继续向下执行代码的现象。

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

避免产生死锁:
1、破坏死锁的四个必要条件
2、加锁顺序一致
3、避免锁未释放的场景
4、资源一次性分配

三、可重入与线程安全

1、概念

~ 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

~ 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2、常见的线程不安全的情况

1、不保护共享变量的函数。
2、函数状态随着被调用,状态发生变化的函数。
3、返回指向静态变量指针的函数。
4、调用线程不安全函数的函数。

3、常见不可重入的情况

1、调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
2、调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3、可重入函数体内使用了静态的数据结构。

4、可重入与线程安全联系

1、函数是可重入的,那就是线程安全的
2、函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
3、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

5、可重入与线程安全区别

1、可重入函数是线程安全函数的一种
2、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
3、如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

相关文章:

Linux之线程互斥

目录 一、问题引入 二、线程互斥 1、相关概念 2、加锁保护 1、静态分配 2、动态分配 3、锁的原理 4、死锁 三、可重入与线程安全 1、概念 2、常见的线程不安全的情况 3、常见的线程安全的情况 4、常见不可重入的情况 5、常见可重入的情况 6、可重入与线程安全联系…...

C++ 拷贝构造函数和运算符重载

目录 一. 拷贝构造函数 1. 引入 2. 拷贝构造的概念 3. 浅拷贝 4. 深拷贝 二. C运算符重载 1. 概念 2. 注意事项 3.举例 一. 拷贝构造函数 1. 引入 我们在创建对象时&#xff0c;能不能创建一个与原先对象一模一样的新对象呢&#xff1f;为了解决这个问题&#x…...

二刷代码随想录算法训练营第二十三天 | 669. 修剪二叉搜索树、108.将有序数组转换为二叉搜索树、538.把二叉搜索树转换为累加树

目录 一、669. 修剪二叉搜索树 二、108. 将有序数组转换为二叉搜索树 三、538. 把二叉搜索树转换为累加树 一、669. 修剪二叉搜索树 题目链接&#xff1a;力扣 文章讲解&#xff1a;代码随想录 视频讲解&#xff1a; 你修剪的方式不对&#xff0c;我来给你纠正一下&#…...

信息抽取在旅游行业的应用:以景点信息抽取为例

开源项目推荐 今天先给大家推荐一个开源项目&#xff0c;多模态AI能力引擎平台: 免费的自然语言处理、情感分析、实体识别、图像识别与分类、OCR识别、语音识别接口&#xff0c;功能强大&#xff0c;欢迎体验。 https://gitee.com/stonedtx/free-nlp-api 场景描述 在旅游行业…...

Linux——基础指令

一、Linux目录结构 1、树形结构 Linux只有一个根目录 / &#xff0c;所有文件都在它下面 2、Linux路径的描述方式 在Linux系统中&#xff0c;路径之间的层级关系&#xff0c;使用&#xff1a; / 来表示 eg&#xff1a; /usr/local/hello.txt 注意&#xff1a; 开头/表示根…...

H5 带网站测速引导页源码

源码名称&#xff1a;带网站测速引导页源码 源码介绍&#xff1a;一款带网站测速功能的引导页源码 需求环境&#xff1a;H5 下载地址&#xff1a; https://www.changyouzuhao.cn/10717.html...

案例分析篇07:数据库设计相关28个考点(23~28)(2024年软考高级系统架构设计师冲刺知识点总结系列文章)

专栏系列文章推荐: 2024高级系统架构设计师备考资料(高频考点&真题&经验)https://blog.csdn.net/seeker1994/category_12593400.html 【历年案例分析真题考点汇总】与【专栏文章案例分析高频考点目录】(2024年软考高级系统架构设计师冲刺知识点总结-案例分析篇-…...

Word中解决插入脚注导致的分页位置错误问题

先放一个截图&#xff1a; 上面的截图中&#xff0c;样式为标题3的段落“四、固执的念头”前插入了连续型分节符&#xff0c;并且该分节符的样式为正文&#xff0c;前后的正文段落中有脚注&#xff0c;结果在分页时&#xff0c;标题3段落“四、固执的念头”后的正文段落自动进入…...

2024/03/14(网络编程·day2)

一、思维导图 二、TCP通信 //服务器 #include<myhead.h>#define SER_PORT 8888 //服务器端口号 #define SER_IP "192.168.117.103" //服务器IP int main(int argc, const char *argv[]) {//1、创建一个套接字int sfd -1;sfd socket(AF_INET,SOCK_STREAM,…...

2024最新陪诊小程序/医院陪诊滴嗒陪诊小程序源码-陪护服务平台陪诊师陪

.系统介绍: 陪护小程序、微信陪诊、、ThinkPHP框架、ThinkPHP6框架、FastAdmin框架、微信小程序。 嘀嗒陪诊小程序功能相对简单,后台也简捷,如果只是做个陪诊服务的小程序也基本能满足了,整体测试了未发现BUG,小程序端也能正常为使用,用户授权接口是老的。 应用背景:人…...

基于单片机的温度控制系统设计

基于单片机的温度控制系统设计 摘要: 最近这些年&#xff0c;随着科学技术的不断发展和进步&#xff0c;单片机技术通过在各行各业中的应用也日臻完善。而温度测控系统也因单片机所特有的强大处理能力、功耗低以及体积小等优点向着小型化和智能化发展。本设计以STC89C52单片机…...

unity3d Animal Controller的Animal组件中Speeds,States和modes基础部分理解

Speeds 速度集是修改你可以做的原始动画,增加或减少运动,旋转,或动画速度。它们与 州 所以,当动物在运动状态下,在飞行或游泳时,你可以有不同的速度 如果你的性格动画是 (已到位), 你一定要调整速度 位置 和 旋转 每一种的价值观 速度装置 …否则,它们不会移动或旋转。 每个速…...

Tomcat详解

1Tomcat安装 下载 Tomcat&#xff1a;首先&#xff0c;您需要从 Tomcat 官方网站&#xff08;http://tomcat.apache.org&#xff09;下载适合您系统的最新版本的 Tomcat 软件包。通常情况下&#xff0c;您会选择一个稳定的版本进行下载。解压缩&#xff1a;下载完成后&#xf…...

SpringCloudAlibaba 网关gateway整合sentinel日志默认路径修改

SpringCloudAlibaba 网关gateway整合sentinel 实现网关限流熔断 问题提出 今天运维突然告诉我 在服务器上内存满了 原因是nacos日志高达3G,然后将日志文件发给我看了一下之后才发现是gateway整合sentinel使用了默认日志地址导致日志生成地址直接存在与根路径下而且一下存在多…...

#LLM入门|Prompt#3.3_存储_Memory

在与语言模型交互时&#xff0c;一个关键问题&#xff1a;记忆缺失使得对话缺乏真正的连续性。 因此&#xff0c;接下来介绍 LangChain 中的储存模块&#xff0c;即如何将先前的对话嵌入到语言模型中的&#xff0c;使其具有连续对话的能力。 当使用 LangChain 中的储存(Memory)…...

基于SSM+Vue的龙腾公司员工信息管理系统设计与实现

​ 1 绪论 1.1研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;规范化管理。这样的大环境让那些止步不前&a…...

使用点链云管家创建瑜伽约课小程序

点链云管家 点链云管家是由上海点链科技开发的门店管理系统&#xff0c;为线下门店商家提供一站式门店运营服务平台解决方案&#xff0c;适用于瑜伽健身、美业、新零售会员制电商、母婴店、宠物店、按摩养生、服装、美容、美甲、汽车服务、商超零售、餐饮、KTV娱乐、干洗等18个…...

【Node.js从基础到高级运用】八、Express 框架入门

Express 框架入门 Express 是一个灵活且广泛使用的 Node.js web 应用框架&#xff0c;它提供了一系列强大特性来帮助开发者创建各种 Web 和移动设备应用。在这一节中&#xff0c;我们将介绍如何安装和配置 Express&#xff0c;并简单探讨其路由和中间件的概念。 安装 Express…...

Unity Timeline学习笔记(2) - PlayableTrack

PlayableTrack 是可自定义播放的轨道。我们可以通过进入轨道后调用自己的函数方法&#xff0c;使用起来也是比较顺手的。 添加轨道 我们点击加号添加 这样就有一个空轨道了&#xff0c;然后我们创建两个测试脚本。 添加脚本 分别是Playable Behaviour和PlayableAsset脚本。…...

Linux的一些常用指令

一、文件中 r w x - 的含义 r&#xff08;read&#xff09;是只读权限&#xff0c; w&#xff08;write&#xff09;是写的权限&#xff0c; x&#xff08;execute&#xff09;是可执行权限&#xff0c; -是没有任何权限。 二、一些指令 # 解压压缩包 tar [-zxvf] 压缩包名…...

09-设计模式 企业场景 面试题

目录 1.简单工厂模式 ​编辑 2.工厂方法模式 3.抽象工厂模式 4.策略模式 5.登录案例(工厂模式+策略模式) 6.责任链设计模式 7.单点登录怎么是实现的? 8.权限认证是如何实现的 9.上传数据的安全性你们怎么控制? 10.你负责项目的时候遇到了哪些比较棘手的问题?怎…...

计算机组成原理-练手题集合【期末复习|考研复习】

前言 总结整理不易&#xff0c;希望大家点赞收藏。 给大家整理了一下计算机组成原理中的各章练手题&#xff0c;以供大家期末复习和考研复习的时候使用。 参考资料是王道的计算机组成原理和西电的计算机组成原理。 计算机组成原理系列文章传送门&#xff1a; 第一/二章 概述和数…...

探索 Spring 框架:企业级应用开发的强大工具

CSDN-个人主页&#xff1a;17_Kevin-CSDN博客 收录专栏&#xff1a;《Java》 目录 一、引言 二、Spring 框架的历史 三、Spring 框架的核心模块 四、Spring 框架的优势 五、Spring 框架的应用场景 六、结论 一、引言 在当今数字化时代&#xff0c;企业级应用开发的需求日…...

java数据结构与算法刷题-----LeetCode47. 全排列 II

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 1. 暴力回溯2. 分区法回溯 此题为46题的衍生题&#xff0c;在46题…...

✅技术社区—MySQL和ES的数据同步策略

使用Canal框架实现MySQL与Elasticsearch&#xff08;ES&#xff09;的数据同步确实可以提高实时搜索的准确性和效率。Canal通过模拟MySQL的binlog日志订阅和解析&#xff0c;实现了数据的实时同步。在这样的同步机制下&#xff0c;ES中的数据可以非常接近于MySQL数据库中的实时…...

LinearLayout和RelativeLayout对比

LinearLayout和RelativeLayout是Android中应用最为广泛的两种布局&#xff0c; 绝大部分UI均可以通过两种布局中的任何一种进行实现&#xff0c;其对比如下&#xff1a; LinearLayout&#xff1a; 1. LinearLayout可以实现子View按照权重分配显示区域&#xff0c;RelativeLayou…...

蓝桥杯深度优先搜索|剪枝|N皇后问题|路径之谜(C++)

搜索&#xff1a;暴力法算法思想的具体实现 搜索&#xff1a;通用的方法&#xff0c;一个问题如果比较难&#xff0c;那么先尝试一下搜索&#xff0c;或许能启发出更好的算法 技巧&#xff1a;竞赛时遇到不会的难题&#xff0c;用搜索提交一下&#xff0c;说不定部分判题数据很…...

大门对楼梯,怎么办?

​ 中国是一个非常重视风水的国家&#xff0c;风水学发扬和流传已有几千年的历史&#xff0c;很多懂风水的人都知道&#xff0c;大门风水是其中非常重要的一环&#xff0c;因为大门风水直接影响全家人的各种运势。大门风水好&#xff0c;能帮助你一臂之力&#xff1b;若大门风…...

解决驱动开发中<stdlib.h> no such file 的问题

前言 在进行驱动开发时&#xff0c;需要使用malloc等函数&#xff0c;导入C库<stdlib.h>出现bug。 嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程&#xff0c;未来预计四个月将高强度更新本专栏&#xff0c;喜欢的可以关注本博主并订阅本专栏&#xff0c;一起讨论…...

Find My工牌|苹果Find My技术与工牌结合,智能防丢,全球定位

工作牌一般是由公司发行的&#xff0c;带有相关工作号及佩戴人信息的卡牌&#xff0c;一般由塑料制作而成。具有醒目.增强内部员工归属感等作用。主要构成为公司名字背景图片员工名字照片。胸牌是一种悬挂或串扣于上衣左方的一种工号牌或介绍小标牌&#xff0c;大多数佩戴在西装…...