[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.rhkb.cn/news/146555.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV之分水岭算法(watershed)

Opencv 中 watershed函数原型&#xff1a; void watershed( InputArray image, InputOutputArray markers ); 第一个参数 image&#xff0c;必须是一个8bit 3通道彩色图像矩阵序列&#xff0c;第一个参数没什么要说的。关键是第二个参数 markers&#xff0c;Opencv官方文档的说…

使用 Ruby 语言来解析开放文档格式 OOXML 文件

在这篇文章中&#xff0c;我们将了解一个开发团队如何解决他们在应用程序中解析数据时遇到的问题。 为了测试 ONLYOFFICE 文档编辑器&#xff0c;我们用Ruby语言开发编写了个docx、xlsx、pptx文件解析器程序&#xff0c;它是免费开源的&#xff0c;被我们放在GitHub和RubyGems…

【一】初步认识数据库

数据库概览数据库 缘起表(Table)的理解用表来定义数据库数据库系统的理解概念层次的理解实例层次的理解 数据库管理系统的理解从用户角度看从系统实现角度看典型的数据库管理系统 数据库语言数据库定义、操纵、控制语言数据库语言 VS 高级语言 内容回顾练习 数据库概览 走马观…

安卓玩机-----给app加注册码 app加弹窗 云注入弹窗

在对接很多工作室业务中有些客户需要在他们自带的有些app中加注册码或者验证码的需求。其实操作起来也很简单。很多反编译软件有自带的注入功能。例如注入弹窗。这个是需要对应的注册码来启动应用。而且是随机id。重新安装app后需要重新注册才可以继续使用&#xff0c;原则上可…

深入学习git

1、git原理及整体架构图 一些常用的命令 git add . 或 git add src/com/ygl/hello/hello.java 指定文件 git commit . 或 git commit src/com/ygl/hello/hello.java 指定文件 git push origin 分支名称 2、git stash的应用场景 场景一&#xff1a;你正在当前分支A开发&…

localStorage实现历史记录搜索功能

&#x1f4dd;个人主页&#xff1a;爱吃炫迈 &#x1f48c;系列专栏&#xff1a;JavaScript &#x1f9d1;‍&#x1f4bb;座右铭&#xff1a;道阻且长&#xff0c;行则将至&#x1f497; 文章目录 为什么使用localStorage如何使用localStorage实现历史记录搜索功能&#xff08…

代码随想录算法训练营第五十二天 | 300. 最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组

1. 最长递增子序列 300. 最长递增子序列 - 力扣&#xff08;LeetCode&#xff09; dp[i] 取决于 i 之前所有的dp class Solution {public int lengthOfLIS(int[] nums) {// dp[i] 第 0 - i 位的递增子序列长度int length nums.length;int[] dp new int[length];Arrays.fil…

基于SSM的微博系统网站的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

如何用ChatGPT学或教英文?5个使用ChatGPT的应用场景!

原文&#xff1a;百度安全验证 AI工具ChatGPT的出现大幅改变许多领域的运作方式&#xff0c;就连「学英文」也不例外&#xff01;我发现ChatGPT应用在英语的学习与教学上非常有意思。 究竟ChatGPT如何改变英文学习者(学生)与教学者(老师)呢&#xff1f; 有5个应用场景我感到…

2023-09-28 monetdb-databae的概念和作用-分析

摘要: 每个数据库对于db,schema以及user,role都有一套自己的设计, 不同数据库间对于相同名字的东西例如database和schema可以说南辕北辙, 例如mysql中schema其实是database的同义词. 本文分析monetdb的database的概念和作用 database的概念和作用: 和mysql的database完全不同…

Linux开发工具之文本编译器vim

目录 Linux编辑器-vim使用 1. vim的基本概念 ● 正常/普通/命令模式(Normal mode) ● 插入模式(Insert mode) ● 末行模式(last line mode) 2. vim的基本操作 ● [正常模式]切换至[插入模式] ● [插入模式]切换至[正常模式] ● [正常模式]切换至[末行模式] ● 退出vi…

步力宝科技爆款产品定位,开创智能物联网新商业

数据显示&#xff0c;中国处于 “亚健康”状态人口数量约占总人口的70%&#xff0c;亚健康是一种临界状态&#xff0c;指介于健康和疾病之间的状态。亚健康是一个动态演变的过程&#xff0c;既有向慢病发展的趋势&#xff0c;也能通过合理的干预使人体重返健康状态&#xff0c;…

网络运营和电子商务有什么区别

大家好&#xff0c;我是网络工程师成长日记实验室的郑老师&#xff0c;您现在正在查看的是网络工程师成长日记专栏&#xff0c;记录网络工程师日常生活的点点滴滴 一个同学他问我&#xff0c;他说学网络运营的话&#xff0c;它是不是电子商务里面的这个东西&#xff1f;像电子大…

南京大学【软件分析】08 Pointer Analysis

文章目录 1. Motivation2. Introduction to Pointer Analysis3. Key Factors of Pointer Analysis3.1 Heap Abstraction3.2 Context Sensitivity3.3 Flow sensitivity3.4 Analysis scope 4. Concerned Statements 1. Motivation 指针分析存在不精确的问题。对于下面的例子&…

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

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

Ubuntu配置深度学习环境(TensorFlow和pyTorch)

文章目录 一、CUDA安装1.1 安装显卡驱动1.2 CUDA安装1.3 安装cuDNN 二、Anaconda安装三、安装TensorFlow和pyTorch3.1 安装pyTorch3.2 安装TensorFlow2 四、安装pyCharm4.1 pyCharm的安装4.2 关联anaconda的Python解释器 五、VScode配置anaconda的Python虚拟环境 前言&#xff…

一维数组和二维数组的使用(char类型)

目录 导读1. 字符数组1.1 字符数组的创建1.2 字符数组的初始化1.3 不同初始化在内存中的不同1.3.1 strlen测试1.3.2 sizeof测试1.3.3 差异原因 1.4 字符数组的使用 2. 数组越界3. 数组作为函数参数博主有话说 导读 我们在前面讲到了 int 类型的数组的创建和使用&#xff1a; 一…

焕新古文化传承之路,AI为古彝文识别赋能

目录 1 古彝文与古典保护 2 古文识别的挑战 2.1 西文与汉文OCR 2.2 古彝文识别难点 3 合合信息&#xff1a;古彝文保护新思路 3.1 图像矫正 3.2 图像增强 3.3 语义理解 3.4 工程技巧 4 总结 1 古彝文与古典保护 彝文指的是云南、贵州、四川等地的彝族人使用的文字&am…

行为型设计模式——责任链模式

摘要 责任链模式(Chain of responsibility pattern): 通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象。 一、责任链模式意图 职责链模式&#xff08;Chain Of Responsibility&#xff09; 是一种行为设…

MAC手动修复『已损坏』问题 终端运行命令报错处理

安装一些第三方软件会出现已损坏的报错提醒&#xff0c;需要用命令sudo xattr -rd com.apple.quarantine进行修复&#xff0c;但是终端提示命令错误&#xff0c;怎么版 错误有几种&#xff1a; No module named ‘pkg_resources’ 这是mac电脑上python2&#xff0c;python3并…