[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.hqwc.cn/news/125561.html

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

相关文章

C++:stl:stack、queue、priority_queue介绍及模拟实现和容量适配器deque介绍

本文主要介绍c中stl的栈、队列和优先级队列并对其模拟实现&#xff0c;对deque进行一定介绍并在栈和队列的模拟实现中使用。 目录 一、stack的介绍和使用 1.stack的介绍 2.stack的使用 3.stack的模拟实现 二、queue的介绍和使用 1.queue的介绍 2.queue的使用 3.queue的…

“在 ArchiMate EA 建模中的组合关系:构建块和依赖关系

简介 在企业架构&#xff08;EA&#xff09;建模领域&#xff0c;结构关系在描绘架构内静态一致性方面起着至关重要的作用。其中一个关键的结构关系是组合关系&#xff0c;这是 ArchiMate 语言中深植的概念&#xff0c;提供了一个全面的框架&#xff0c;用于表达元素如何组合形…

正点原子嵌入式linux驱动开发——TF-A移植

经过了之前的学习&#xff0c;除了TF-A的详细启动流程仍待更新&#xff0c;TF-A的使用和其对应的大致启动流程已经进行过了学习。但是当我们实际做产品时&#xff0c;硬件平台肯定会和ST官方的有区别&#xff0c;比如DDR容量会改变&#xff0c;自己的硬件没有使用到官方EVK开发…

Ubuntu使用cmake和vscode开发自己的项目,引用自己的头文件和openCV

创建文件夹 mkdir my_proj 继续创建include 和 src文件夹&#xff0c;形成如下的目录结构 用vscode打开项目 创建add.h #ifndef ADD_H #define ADD_Hint add(int numA, int numB);#endif add.cpp #include "add.h"int add(int numA, int numB) {return numA nu…

盒子阴影和网页布局

盒子阴影 box-shadow: 10px 10px 10px 4px rgba(0,0,0,.3);//最后一个是透明度 传统网页布局的三种方式 标准流 就是按照规定好的默认方式排列 1.块级元素&#xff1a;div、hr、p、h1~h2、ul、ol、dl、form、table 行内元素会按照书顺序&#xff0c;从左到右顺序排列&#…

黑豹程序员-架构师学习路线图-百科:Git/Gitee(版本控制)

文章目录 1、什么是版本控制2、特点3、发展历史4、SVN和Git比较5、Git6、GitHub7、Gitee&#xff08;国产&#xff09;8、Git的基础命令 1、什么是版本控制 版本控制系统&#xff08; Version Control &#xff09;版本控制是一种管理和跟踪软件开发过程中的代码变化的系统。它…

博途1200/1500 ALT指令

SMART PLC的ALT指令实现代码,请查看下面文章博客 SMART PLC如何构造ALT指令_smart200类似alt指令-CSDN博客单按钮启停这些老生常谈的问题,很多人感兴趣。这篇博文讨论下不同的实现方法,希望对大家有所帮助。指令虽然简单,但是在编程的时候合理使用对我们高效率编程帮助还是…

C语言学习系列->联合体and枚举

文章目录 前言联合体概述联合体的特点联合体大小的计算优点练习 枚举概述优点使用 前言 在上一篇文章中&#xff0c;小编将结构体的学习笔记整理出来了。现在&#xff0c;小编将枚举和联合体笔记分享给大家。枚举和联合体与结构体一样&#xff0c;都是自定义类型&#xff0c;在…

Bug:elementUI样式不起作用、Vue引入组件报错not found等(Vue+ElementUI问题汇总)

前端问题合集&#xff1a;VueElementUI 1. Vue引用Element-UI时&#xff0c;组件无效果解决方案 前提&#xff1a; 已经安装好elementUI依赖 //安装依赖 npm install element-ui //main.js中导入依赖并在全局中使用 import ElementUI from element-ui Vue.use(ElementUI)如果此…

SpringCloud(二)Docker、Spring AMQP、ElasticSearch

文章目录 DockerDocker与虚拟机Docker架构镜像、容器、镜像托管平台Docker架构Docker实践 Spring AMQP简单使用案例工作队列- WorkQueue发布订阅服务FanoutExchangeDirectExchangeTopicExchange 消息转换器 ElasticSearch倒排索引IK分词器IK分词拓展与停用字典 操作索引库mappi…

C/C++学习 -- HMAC算法

1. HMAC算法概述 HMAC&#xff0c;全称为HMAC-MD5、HMAC-SHA1、HMAC-SHA256等&#xff0c;是一种在数据传输中验证完整性和认证来源的方法。它结合了哈希函数和密钥&#xff0c;通过在数据上应用哈希函数&#xff0c;生成一个带密钥的散列值&#xff0c;用于验证数据的完整性。…

Septentrio接收机二进制的BDS b2b改正数解码

Galileo的HAS和BDS B2b改正数为实时PPP提供了可能&#xff0c;要实现实时PPP解算&#xff0c;必须对对应的数据进行解码。由于没有做过解码的工作&#xff0c;现结合qzsl6tool代码对Septentrio的解码代码进行学习。 1. 二进制枕头的识别和解码 定义一个读取数据的类&#xff…