[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

避免死锁的方法

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

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

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

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

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

相关文章

CV面试知识点总结

一.卷积操作和图像处理中的中值滤波操作有什么区别&#xff1f; 1.1卷积操作 卷积操作是一种线性操作&#xff0c;通常用于特征的提取&#xff0c;通过卷积核的加权求和来得到新的像素值。1.2中值滤波 原文&#xff1a; https://blog.csdn.net/weixin_51571728/article/detai…

【Linux】UDP的服务端 + 客户端

文章目录 &#x1f4d6; 前言1. TCP和UDP2. 网络字节序2.1 大小端字节序&#xff1a;2.2 转换接口&#xff1a; 3. socket接口3.1 sockaddr结构&#xff1a;3.2 配置sockaddr_in&#xff1a;3.3 inet_addr&#xff1a;3.4 inet_ntoa&#xff1a;3.5 bind绑定&#xff1a; 4. 服…

【面试经典150 | 矩阵】旋转图像

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;原地旋转方法二&#xff1a;翻转代替旋转 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带…

线性表的链式存储结构——链表

一、顺序表优缺点 优点&#xff1a;我们知道顺序表结构简单&#xff0c;便于随机访问表中任一元素&#xff1b; 缺点&#xff1a;顺序存储结构不利于插入和删除&#xff0c;不利于扩充&#xff0c;也容易造成空间浪费。 二、链表的定义 ①&#xff1a;概念&#xff1a; 用一组任…

Springboot+vue的在线试题题库管理系统(有报告),Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的在线试题题库管理系统&#xff08;有报告&#xff09;&#xff0c;Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的在线试题题库管理系统&#xff0c;采用M&…

PHP 数码公司运营管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 数码公司运营管理系统系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 php 数码公司运营管理系统 代码 https://download.csdn.net/download/qq_41…

基于 jasypt 实现spring boot 配置文件脱敏

前言 在项目构建过程中&#xff0c;保护敏感信息的安全性至关重要&#xff0c;为了提高系统的安全性能&#xff0c;我们采用了Jasypt来对配置文件中的敏感信息进行加密处理&#xff0c;以确保系统的机密信息不被轻易泄露。 步骤 添加Maven依赖 首先&#xff0c;我们需要添加…

【kubernetes】kubernetes中的StatefulSet使用

TOC 1 为什么需要StatefulSet 常规的应用通常使用Deployment&#xff0c;如果需要在所有机器上部署则使用DaemonSet&#xff0c;但是有这样一类应用&#xff0c;它们在运行时需要存储一些数据&#xff0c;并且当Pod在其它节点上重建时也希望这些数据能够在重建后的Pod上获取&…

buuctf-[Zer0pts2020]Can you guess it?

点击source&#xff0c;进入源代码 <?php include config.php; // FLAG is defined in config.phpif (preg_match(/config\.php\/*$/i, $_SERVER[PHP_SELF])) {exit("I dont know what you are thinking, but I wont let you read it :)"); }if (isset($_GET[so…

【算法学习】-【双指针】-【复写零】

LeetCode原题链接&#xff1a;1089. 复写零 下面是题目描述&#xff1a; 给你一个长度固定的整数数组 arr &#xff0c;请你将该数组中出现的每个零都复写一遍&#xff0c;并将其余的元素向右平移。 注意&#xff1a;请不要在超过该数组长度的位置写入元素。请对输入的数组 …

【浅记】分而治之

归并排序 算法流程&#xff1a; 将数组A[1,n]排序问题分解为A[1,n/2]和A[n/21,n]排序问题递归解决子问题得到两个有序的子数组将两个子数组合并为一个有序数组 符合分而治之的思想&#xff1a; 分解原问题解决子问题合并问题解 递归式求解 递归树法 用树的形式表示抽象递…

7.网络原理之TCP_IP(下)

文章目录 4.传输层重点协议4.1TCP协议4.1.1TCP协议段格式4.1.2TCP原理4.1.2.1确认应答机制 ACK&#xff08;安全机制&#xff09;4.1.2.2超时重传机制&#xff08;安全机制&#xff09;4.1.2.3连接管理机制&#xff08;安全机制&#xff09;4.1.2.4滑动窗口&#xff08;效率机制…

uni-app:实现元素在屏幕中的居中(绝对定位absolute)

一、实现水平居中 效果 代码 <template><view><view class"center">我需要居中</view></view> </template><style>.center {position: absolute;left:50%;transform: translateX(-50%);border:1px solid black;} </s…

freertos简介与移植

freertos是一个可裁剪的小型rtos系统&#xff0c;特点&#xff1a; 支持抢占式&#xff0c;合作式和时间片调度saferos衍生自freertos&#xff0c;更完整提供了一个用于低功耗的tickless模式系统的组件在创建时可以选择动态或者静态的ram&#xff0c;例如任务&#xff0c;消息…

快排三种递归及其优化,非递归和三路划分

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 快排简介&#xff1a; 快排的三种递归实现&#xff1a; Hoare&#xff1a; 挖坑&#xff1a; 双指针&#xff1a; 小区间优化&#xff1a; 三数取中优化&#xff1a; 快排非递归实现&#xff1a; 快排的三路划…

计算机网络(一):概述

参考引用 计算机网络微课堂-湖科大教书匠计算机网络&#xff08;第7版&#xff09;-谢希仁 1. 计算机网络在信息时代的作用 计算机网络已由一种通信基础设施发展成为一种重要的信息服务基础设施计算机网络已经像水、电、煤气这些基础设施一样&#xff0c;成为我们生活中不可或…

《CTFshow-Web入门》10. Web 91~110

Web 入门 索引web91题解总结 web92题解总结 web93题解 web94题解 web95题解 web96题解 web97题解 web98题解 web99题解总结 web100题解 web101题解 web102题解 web103题解 web104题解 web105题解总结 web106题解 web107题解 web108题解 web109题解 web110题解 ctf - web入门 索…

红外遥控器 数据格式,按下及松开判断

红外遥控是一种无线、非接触控制技术&#xff0c;具有抗干扰能力强&#xff0c;信息传输可靠&#xff0c;功耗低&#xff0c;成本低&#xff0c;易实现等显著优点&#xff0c;被诸多电子设备特别是家用电器广泛采用&#xff0c;并越来越多的应用到计算机系统中。 同类产品的红…

iOS 视频压缩 mov转mp4 码率

最近还是因为IM模块的功能&#xff0c;IOS录制MOV视频发送后&#xff0c;安卓端无法播放&#xff0c;迫不得已兼容将MOV视频转为MP4发送。 其中mov视频包括4K/24FPS、4K/30FPS、4K/60FPS、720p HD/30FPS、1080p HD/30FPS、1080p HD/60FPS&#xff01; 使用AVAssetExportSessi…

使用sqlmap获取数据步骤

文章目录 1.使用sqlmap获取所有数据库2.使用sqlmap获取当前连接数据库3.使用sqlmap获取当前数据库下所有表名4.使用sqlmap获取当前数据库下某个表下所有列名5.使用sqlmap获取当前数据库下某个表下指定字段的数据6.测试当前用户是否是管理员7.使用burpsqlmap批量检测8.脱库命令9…