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

[Linux]线程互斥

[Linux]线程互斥

文章目录

  • [Linux]线程互斥
    • 线程并发访问问题
    • 线程互斥控制--加锁
      • pthread_mutex_init函数
      • pthread_mutex_destroy函数
      • pthread_mutex_lock函数
      • pthread_mutex_unlock函数
      • 锁相关函数使用示例
      • 使用锁的细节
      • 加锁解锁的实现原理
    • 线程安全
      • 概念
      • 常见的线程不安全的情况
      • 常见的线程安全的情况
      • 常见不可重入的情况
      • 常见可重入的情况
      • 可重入与线程安全联系
      • 可重入与线程安全区别
    • 死锁
      • 概念
      • 死锁的四个必要条件
      • 避免死锁的方法

线程并发访问问题

为了了解线程并发访问问题,我们通过一个示例来理解。首先,我们要知道计算机完成对一个内存中的数据的减法操作对应的三条汇编指令,第一步是将内存中的数据加载到CPU的寄存器中,第二步是将寄存器中的数据进行减法操作,第三步是将计算后的数据写回到内存中。

image-20230927141046581

存在以下场景,两个线程都要对同一个数据进行操作,两个线程看到的同一个地址空间,当第一个线程将该数据加载到CPU中,并完成计算要写回的时候,由于操作系统的调度策略,切换成了第二个线程,第二个线程也做了将该数据加载到CPU中,并完成计算,并且成功写回,此时又轮到第一个线程被调度了,第一个线程会继续执行之前未完成的操作,将数据写回。由于第一个线程对数据的计算没有写回内存,第二个线程操作前的数据和第一个线程操作前的数据是一样的,因此第一个线程回写时会将第二个线程的操作覆盖,造成并发访问问题。

image-20230927142734766

以上场景就是由于并发访问导致的多个线程的数据不一致的问题。

提出如下概念:

  • 线程共享的资源被称为临界资源
  • 线程中访问临界资源的代码被称为临界区,不访问临界资源的代码被称为非临界区
  • 为了避免线程并发访问问题,需采用加锁的方式让线程进行互斥访问

编写如下代码模拟抢票操作产生的并发访问问题:

#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;int tickets = 10000;//模拟共享资源void *threadRoutine(void *args)
{char *name = static_cast<char*>(args);while(true){if(tickets > 0){usleep(1000);//模拟抢票操作cout << name << " get a ticket: " << tickets-- << endl;}else{break;}}return nullptr;
}int main()
{pthread_t tids[4];int n = sizeof(tids)/sizeof(tids[0]);for (int i = 0; i < n; i++)//创建4个线程{char* tname = new char[64];snprintf(tname, 64, "thread->%d", i + 1);pthread_create(tids + i, nullptr, threadRoutine, tname);}for (int i = 0; i < n; i++)//回收线程{pthread_join(tids[i], nullptr);}return 0;
}

编译代码运行并查看结果:

image-20230927151914632

由于在执行判断逻辑时,多个线程都成功进入了临界区,当其中一个线程将票数改为0时,其他线程已经在执行临界区代码无法终止,导致最终票数为负数。

线程互斥控制–加锁

线程互斥是指在多线程编程中,通过使用某种机制来保护共享资源,以确保在任意时刻只有一个线程能够访问或修改共享资源。

锁是Linux操作系统原生线程库中提供的pthread_mutex_t数据类型,通过对锁的使用能够完成线程的互斥控制。

锁的特性: 一把锁只能同时被一个线程使用。

pthread_mutex_init函数

Linux操作系统下提供了pthread_mutex_init函数用于初始化锁。

//pthread_mutex_init所在的头文件和函数声明
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,  const pthread_mutexattr_t *restrict attr);
  • pthread_mutex_init函数用于初始化。
  • mutex参数: 要初始化的锁。
  • attr参数: 给锁设置的属性,默认传入空指针。
  • 全局变量锁可以采用初始化时pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER的方式进行初始化。

pthread_mutex_destroy函数

Linux操作系统下提供了pthread_mutex_destroy函数用于销毁锁。

//pthread_mutex_destroy所在的头文件和函数声明
#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • pthread_mutex_destroy函数用于局部变量锁的销毁。
  • mutex参数: 要销毁的锁。

pthread_mutex_lock函数

Linux操作系统下提供了pthread_mutex_lock函数用于申请锁。

//pthread_mutex_lock所在的头文件和函数声明
#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);
  • pthread_mutex_lock函数用于申请锁。
  • 锁被其他线程申请后,调用该函数会阻塞等待锁被释放然后申请锁。

pthread_mutex_unlock函数

Linux操作系统下提供了pthread_mutex_unlock函数用于释放锁。

//pthread_mutex_lock所在的头文件和函数声明
#include <pthread.h>int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_unlock函数用于释放锁。

锁相关函数使用示例

为了验证加锁控制线程互斥能够解决线程并发访问问题,对前文抢票模拟代码进行改进,具体代码如下:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>using namespace std;int tickets = 1000; // 模拟共享资源class Tdata//作为传入线程执行函数接收的类
{
public:Tdata(char *name, pthread_mutex_t *pmutex) : _name(name), _pmutex(pmutex){}
public:string _name;pthread_mutex_t *_pmutex;
};void *threadRoutine(void *args)
{Tdata *td = static_cast<Tdata *>(args);while(true){pthread_mutex_lock(td->_pmutex);//申请锁if(tickets > 0){usleep(1000);//模拟抢票操作cout << td->_name << " get a ticket: " << tickets-- << endl;pthread_mutex_unlock(td->_pmutex);//释放锁}else{pthread_mutex_unlock(td->_pmutex);//释放锁break;}usleep(200);//模拟抢票后续操作,并且避免同一个线程总能申请到锁}return nullptr;
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr); // 初始化锁pthread_t tids[4];int n = sizeof(tids) / sizeof(tids[0]);for (int i = 0; i < n; i++) // 创建4个线程{char tname[64];snprintf(tname, 64, "thread->%d", i + 1);Tdata* td = new Tdata(tname, &mutex);pthread_create(tids + i, nullptr, threadRoutine, (void *)td);}for (int i = 0; i < n; i++) // 回收线程{pthread_join(tids[i], nullptr);}pthread_mutex_destroy(&mutex); // 销毁锁return 0;
}

编译代码运行并查看结果:

image-20230927170812560

由于每个线程在访问共享资源前都需要申请锁,线程操作共享资源的操作是互斥的,因此多个线程对共享资源的操作完全是串行化的,不会造成多个线程进入临界区,但是进入临界区的条件不满足了的情况。

使用锁的细节

  1. 凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁。
  2. 每一个线程访问临界区之前,得加锁,加锁本质是给临界区加锁,加锁的粒度尽量要细一些。
  3. 线程访问临界区的时候,需要先加锁,所有线程都必须要先看到同一把锁,因此锁本身就是公共资源,加锁和解锁本身就是原子的保证了锁的并发访问是安全的。
  4. 临界区可以是一行代码,可以是一批代码,进入临界区的线程也可能被切换,但是由于进入临界区时锁被改线程拿走了,其他线程申请不到锁,因此不会存在并发访问问题。
  5. 申请不到锁的线程只能等待正是体现互斥带来的串行化的表现, 因此申请不到锁的线程无法执行临界区代码,申请到锁的线程最终一定会将临界区的代码执行完,原子性就体现在这里。
  6. 解锁的过程也被设计成为原子的!

加锁解锁的实现原理

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。以下是加锁实现的伪代码:

image-20230929135514462

  • 加锁伪代码的主要步骤如下: 第一步向%al寄存器中写入0,第二步执行exchange指令将%al寄存器中的数据和内存中锁中的数据(锁初始化后锁中数据大于0)进行交换,第三步判断%al中的数据,大于0即是申请到了锁,否则就是锁被别人申请走了。

  • 第二步数据交换,就是加锁的本质,当线程执行这段代码把锁中数据交换成0后,锁中原有数据就被该线程“私有化了”,即使线程切换寄存器中的数据也会作为线程上下文被切走,其他线程在执行加锁的代码交换到%al的寄存器数据都是0,因此只能挂起等待,保证了锁的互斥性。

  • 加锁的本质是一条命令,保证了加锁的原子性, 代码执行的基本单位是一条指令,因此加锁过程一定是要么没做,要么做完的,是具有原子性的。

  • 锁被申请到后,其他线程无法申请到锁, 由于加锁的本质是一条交换命令,因此一个线程执行交换命令完成加锁后,其他线程想加锁也只是使用交换命令将0交换,无法申请到锁。

以下是解锁实现的伪代码:

image-20230929141317702

解锁的本质是将大于0的数据写回至内存中的锁,由于只有一条指令,因此解锁也是原子性的。

线程安全

概念

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

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

常见的线程不安全的情况

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

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

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

常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

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

可重入与线程安全区别

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

死锁

概念

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

死锁的四个必要条件

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

死锁形成示意图:

image-20230929191625084

互斥:资源1和资源2只能被线程1和线程2中的一方使用

请求与保持条件:线程1申请资源2并且占有资源1不释放,线程2申请资源1并且占有资源2不释放

循环等待条件:线程1占有资源1申请资源2,线程2占有资源2申请资源1

不剥夺条件:线程1不能强行剥夺线程2已占有的资源2,线程2不能强行剥夺线程1已占有的资源1

避免死锁的方法

避免死锁的方法是破坏死锁四个必要条件中的任意一个条件,具体方法如下:

  • 不加锁:对应互斥条件
  • 主动释放锁:对应请求与保持条件
  • 按照顺序申请锁:对应循环等待条件
  • 控制线程统一释放锁:对应不剥夺条件

说明: 申请锁的线程和释放锁的线程可以是不同的线程。

相关文章:

[Linux]线程互斥

[Linux]线程互斥 文章目录 [Linux]线程互斥线程并发访问问题线程互斥控制--加锁pthread_mutex_init函数pthread_mutex_destroy函数pthread_mutex_lock函数pthread_mutex_unlock函数锁相关函数使用示例使用锁的细节加锁解锁的实现原理 线程安全概念常见的线程不安全的情况常见的…...

leetcode-239-滑动窗口最大值

题意描述&#xff1a; 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例&#xff1a; 输入&#xff1a;nums [1,3,-1,…...

基于大语言模型的智能问答系统应该包含哪些环节?

一个完整的基于 LLM 的端到端问答系统&#xff0c;应该包括用户输入检验、问题分流、模型响应、回答质量评估、Prompt 迭代、回归测试&#xff0c;随着规模增大&#xff0c;围绕 Prompt 的版本管理、自动化测试和安全防护也是重要的话题&#xff0c;本篇文章就来探索下这个过程…...

【Cesium创造属于你的地球】相机系统

相机系统里面有setView&#xff0c;flyTo&#xff0c;lookAt&#xff0c;viewBoundingsphere这几种方法&#xff0c;以下是相关的使用方法&#xff0c;学起来&#xff01;&#xff01;&#xff01; setView 该方法可以直接切换相机视口&#xff0c;从而不需要通过一个飞入的效…...

运维困局下确保系统稳定的可行性

业务高速发展背后的困局 随着业务的快速发展&#xff0c;运维体系也逐步的完善起来。业务的稳定性和服务质量也在监控、可用性等体系的相互环抱下健康地成长。所有的问题、故障及影响稳定性的因素都在可控、可收敛的范围内&#xff0c;一切都向着好的方向发展。 这一切的背后…...

springmvc中DispatcherServlet关键对象

以下代码为 spring boot 2.7.15 中自带的 spring 5.3.29 RequestMappingInfo 请求方法相关信息封装&#xff0c;对应的信息解析在 RequestMappingHandlerMapping 的 createRequestMappingInfo() 中实现。 对于 RequestMapping 赋值的相关信息进行解析 protected RequestMappi…...

某微e-office协同管理系统存在任意文件读取漏洞复现 CNVD-2022-07603

目录 1.漏洞概述 2.影响版本 3.漏洞等级 4.漏洞复现 5.Nuclei自动化扫描POC 某微e-office协同管理系统存在任意文件读取漏洞分析 CNVD-2022-07603https://blog.csdn.net/qq_41490561/article/details/133469649...

消息驱动 —— SpringCloud Stream

Stream 简介 Spring Cloud Stream 是用于构建消息驱动的微服务应用程序的框架&#xff0c;提供了多种中间件的合理配置 Spring Cloud Stream 包含以下核心概念&#xff1a; Destination Binders&#xff1a;目标绑定器&#xff0c;目标指的是 Kafka 或者 RabbitMQ&#xff0…...

使用Apache HttpClient爬取网页内容的详细步骤解析与案例示例

Apache HttpClient是一个功能强大的开源HTTP客户端库&#xff0c;本文将详细介绍如何使用Apache HttpClient来爬取网页内容的步骤&#xff0c;并提供三个详细的案例示例&#xff0c;帮助读者更好地理解和应用。 一、导入Apache HttpClient库 在项目的pom.xml文件中添加依赖&a…...

传输层协议—UDP协议

传输层协议—UDP协议 文章目录 传输层协议—UDP协议传输层再谈端口号端口号范围划分pidofnetstat UDP协议端格式UDP报文UDP特点UDP缓冲区基于UDP的应用层协议 传输层 在学习HTTP/HTTPS等应用层协议时&#xff0c;为了方便理解&#xff0c;可以简单认为HTTP将请求和响应直接发送…...

【改造中序遍历】 538. 把二叉搜索树转换为累加树

538. 把二叉搜索树转换为累加树 解题思路 改造中序遍历算法因为中序遍历的结果都是有顺序的 升序排序&#xff0c;那么如果先遍历右子树 在遍历左子树 那么结果就是降序的最后我们设置一个变量 累加所有的中间值 那么得到的结果就是比当前节点大的所有节点的值 /*** Definiti…...

2022年11月工作经历

11月 招聘 最近招聘C程序员和黑盒测试员。由于第一次招聘不知道如何处理&#xff0c;不断和同事沟通&#xff0c;摸索出一套简单的规则。C程序员&#xff1a;力扣随机第二题&#xff0c;如果运气不好可以再随机一两次。黑盒测试员&#xff1a;力扣随机第二题或第三题&#xff…...

使用广播信道的数据链路层

使用广播信道的数据链路层 ​ 广播信道可以一对多通信。局域网使用的就是广播信道。局域网最主要的特点就是网络为一个单位所拥有&#xff0c;且地理范围和站点数目有限。局域网可按网络拓扑进行分为星形网、环形网、总线网。传统的以太网就是总线网&#xff0c;后来又演变为星…...

第3章-指标体系与数据可视化-3.1.2-Seaborn绘图库

目录 3.1.2 Seaborn绘图库 1. 带核密度估计的直方图 2. 二元分布图 一维正态分布 联合分布...

excel中将一个sheet表根据条件分成多个sheet表

有如下excel表&#xff0c;要求&#xff1a;按月份将每月的情况放在一个sheet中。 目测有6个月&#xff0c;就应该有6个sheet&#xff0c;每个sheet中体现本月的情况。 一、首先增加一个辅助列&#xff0c;月份&#xff0c;使用month函数即可。 填充此列所有。然后复制【月份】…...

案例突破——再探策略模式

再探设计模式 一、背景介绍二、 思路方案三、过程1. 策略模式基本概念2. 策略模式类图3. 策略模式基本代码策略类抽象策略类Context类客户端 4. 策略模式还可以进行优化的地方5. 对策略模式的优化&#xff08;配置文件反射&#xff09; 四、总结五、升华 一、背景介绍 在做项目…...

uboot启动流程-涉及lowlevel_init汇编函数

一. uboot启动流程涉及函数 之前文章简单分析了 uboot启动流程的开始&#xff0c;从链接脚本文件 u-boot.lds 中&#xff0c;我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的 _start函数。 _start函数&#xff1a;调用了 reset 函数&#xff0c;reset 函数内部&…...

质数距离 - 如何在较合理的时间复杂度内求2e9范围内的质数

求l、r之间的质数&#xff0c;范围在2e9&#xff0c;但l、r的差值不大&#xff0c;在1e6范围内 先求出 内的质数&#xff0c;然后拿这个指数去筛[l, r]范围内的即可 #include<bits/stdc.h> #define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); #define endl \…...

八、3d场景的区域光墙

在遇到区域展示的时候我们就能看到炫酷的区域选中效果&#xff0c;那么代码是怎么编辑的呢&#xff0c;今天咱们就好好说说&#xff0c;下面看实现效果。 思路&#xff1a; 首先&#xff0c;光墙肯定有多个&#xff0c;那么必须要创建一个新的js文件来作为他的原型对象。这个光…...

深入探讨 Presto 中的缓存

【squids.cn】 全网zui低价RDS&#xff0c;免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 Presto是一种流行的开源分布式SQL引擎&#xff0c;使组织能够在多个数据源上大规模运行交互式分析查询。缓存是一种典型的提高 Presto 查询性能的优化技术。它为 Prest…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

Redis:现代应用开发的高效内存数据存储利器

一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发&#xff0c;其初衷是为了满足他自己的一个项目需求&#xff0c;即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源&#xff0c;Redis凭借其简单易用、…...

宇树科技,改名了!

提到国内具身智能和机器人领域的代表企业&#xff0c;那宇树科技&#xff08;Unitree&#xff09;必须名列其榜。 最近&#xff0c;宇树科技的一项新变动消息在业界引发了不少关注和讨论&#xff0c;即&#xff1a; 宇树向其合作伙伴发布了一封公司名称变更函称&#xff0c;因…...

jmeter聚合报告中参数详解

sample、average、min、max、90%line、95%line,99%line、Error错误率、吞吐量Thoughput、KB/sec每秒传输的数据量 sample&#xff08;样本数&#xff09; 表示测试中发送的请求数量&#xff0c;即测试执行了多少次请求。 单位&#xff0c;以个或者次数表示。 示例&#xff1a;…...

MySQL 部分重点知识篇

一、数据库对象 1. 主键 定义 &#xff1a;主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 &#xff1a;确保数据的完整性&#xff0c;便于数据的查询和管理。 示例 &#xff1a;在学生信息表中&#xff0c;学号可以作为主键&#xff…...

Vue ③-生命周期 || 脚手架

生命周期 思考&#xff1a;什么时候可以发送初始化渲染请求&#xff1f;&#xff08;越早越好&#xff09; 什么时候可以开始操作dom&#xff1f;&#xff08;至少dom得渲染出来&#xff09; Vue生命周期&#xff1a; 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...

自然语言处理——文本分类

文本分类 传统机器学习方法文本表示向量空间模型 特征选择文档频率互信息信息增益&#xff08;IG&#xff09; 分类器设计贝叶斯理论&#xff1a;线性判别函数 文本分类性能评估P-R曲线ROC曲线 将文本文档或句子分类为预定义的类或类别&#xff0c; 有单标签多类别文本分类和多…...

使用SSE解决获取状态不一致问题

使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件&#xff0c;这个上传文件是整体功能的一部分&#xff0c;文件在上传的过程中…...

C++_哈希表

本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、基础概念 1. 哈希核心思想&#xff1a; 哈希函数的作用&#xff1a;通过此函数建立一个Key与存储位置之间的映射关系。理想目标&#xff1a;实现…...