[Linux]线程互斥

news2024/11/26 16:53:31

[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.coloradmin.cn/o/1056293.html

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

相关文章

蓝桥杯每日一题2023.10.2

时间显示 - 蓝桥云课 (lanqiao.cn) 题目描述 题目分析 输入为毫秒&#xff0c;故我们可以先将毫秒转化为秒&#xff0c;由于只需要输出时分&#xff0c;我们只需要将天数去除即可&#xff0c;可以在这里多训练一次天数判断 #include<bits/stdc.h> using namespace std…

检索qpython文件夹下.py

需求口令 检索 /storage/emulated/0/qpython 文件夹下的.py文件 编号原文件名&#xff1a;复制到/storage/emulated/0/qpython/py文件/ 没有文件夹就创建 检索qpython文件夹下.py&#xff0c;复制到py文件单独路径 根据这个提问清单和步骤&#xff0c;我们需要完成以下任务&…

Audio2Face的工作原理

预加载一个3D数字人物模型(Digital Mark),该模型可以通过音频驱动进行面部动画。 用户上传音频文件作为输入。 将音频输入馈送到预训练的深度神经网络中。 Audio2Face加载预制的3d人头mesh 3D数字人物面部模型由大量顶点组成,每个顶点都有xyz坐标。 深度神经网络输入音频特征,…

C++基础语法(继承)

终于&#xff0c;经过一路的过关斩将&#xff0c;我们来到了继承面前。还记得在最初学习类于对象时&#xff0c;那个对封装概念一直模糊不清的自己&#xff0c;还记得被模板&#xff0c;被迭代器折磨的日日夜夜吗&#xff1f;这一路你挺过来了&#xff0c;你失去了一些东西&…

zkLogin构建者的最佳实践和业务思考

随着zkLogin在Sui主网上线&#xff0c;构建者可以开始为其应用程序提供丝滑的帐户创建服务。与任何新技术集成一样&#xff0c;构建者需要考虑许多重要的问题&#xff0c;以降低风险并成功优化。 本文概述了其中一些考虑因素&#xff0c;并突出了zkLogin文档中提供的明确指导。…

Linux-centos系统安装MySql5.7

1.配置yum仓库 1.1配置yum仓库 rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 1.2 安装Mysql yum库 rpm -Uvh http://repo.mysql.com//mysql57-community-release-el7-7.noarch.rpm 2.使用yum安装Msql 说明&#xff1a;下载大约5分钟左右 yum -y install mysq…

基于Java的教学评价管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言系统功能结构图系统ER图具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划…

分页前后端完整代码

一、后端代码 User实体类要继承PageVo package com.like.entity;import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.like.common.PageVo; import…

web:[SUCTF 2019]EasySQL

题目 点进页面&#xff0c;页面显示为 没有其他的提示信息&#xff0c;查看源代码看看是否有提示 什么都没有&#xff0c;由题目名为easysql&#xff0c;能想到这道题与sql注入有关 先输入1查看回显 再输入其他数字或字母看看回显 都是无显示 先验证是否存在sql注入 构造pay…

python爬取csdn个人首页中的所有封面

#爬取csdn个人首页中的所有封面 import requests import json import reurlhttps://blog.csdn.net/community/home-api/v1/get-business-list? headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safar…

Scala第十二章节

Scala第十二章节 1.读取数据 2.写入数据 3.案例&#xff1a;学生成绩表 scala总目录 文档资料下载

Origin分段显示柱状图

注意这里生成的是柱状图&#xff0c;而不是直方图。因此用到的是plot -> column/bar/pie -> stacked column&#xff1a; 而不是Statistical->histogram。 先上最终的作图效果&#xff1a; 单个柱的柱状图 第一步先填充数据&#xff0c;如图左所示&#xff0c;然后选…

P1540 [NOIP2010 提高组] 机器翻译(模拟)

[NOIP2010 提高组] 机器翻译 题目背景 小晨的电脑上安装了一个机器翻译软件&#xff0c;他经常用这个软件来翻译英语文章。 题目描述 这个翻译软件的原理很简单&#xff0c;它只是从头到尾&#xff0c;依次将每个英文单词用对应的中文含义来替换。对于每个英文单词&#xf…

Java类与对象

文章目录 引出概念快速入门对象内存布局属性概念创建对象访问属性对象分配机制成员方法&#x1f996;注意事项和细节 引出 ●看一个养猫猫问题 张老太养了两只猫猫: 一只名字叫小白, 今年3岁, 白色. 还有一只叫小花, 今年100岁, 花色. 请编写一个程序, 当用户输入小猫的名字时…

Qt自定义菜单

Qt开发过程中&#xff0c;弹出菜单时我们一般使用QMenu,但是QMenu都是一条项固定的格式&#xff0c;如查想要自己的设计界面就没法使用默认的Action项了&#xff0c;因此我们得用自定义的QMenu。 本篇介绍使用自定义的QMenu设计出UI。我们使用QWidget QWidgetAction来实现。Q…

代码随想录 Day8 栈(FILO)与队列(FIFO) LeetCode T232 用栈实现队列 LeetCodeT225 用队列实现栈

题目详细思路来自于:代码随想录 (programmercarl.com) 栈和队列都是大家不陌生的数据结构,我们之前的栈和队列一般是用数组或链表来实现的 , 这里我们给出实现方式,用于帮助更好的理解. 1.用链表实现栈 /* 基于链表实现的栈 */ class LinkedListStack { private ListNode sta…

记录我的网站的上线的全过程

我的网站开发全过程记录: 提示: 大部分是cv代码,少部分样式进行了修改和借鉴,主要花费时间在部署上面,出现的问题 开始: 三个博主: 在掘金上认识了搜狐前端-- 英杰 , 掘金链接:yingjieweb 的个人主页 - 动态 - 掘金 偶然间看见了英杰的网站,感觉非常不错,产生自己弄的想法,…

mybatis-spring集成数据库连接池开启注解式开发

目录 1. 引入依赖包 2. 集成配置文件 2.1 开启注解式开发 2.2 spring引入外部配置文件 2.3 数据库连接池 2.4 spring整合mybatis 2.5 自动代理 3. 注解式开发的几个常用注解 4. spring-test 附录一&#xff1a;spring常用注解 1. 引入依赖包 <!--spring整合mybat…

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

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

Mac安装Ecplise产品报错:dose not contain the JNI_CreateJavaVM symbol

1. 絮絮叨叨 工作中需要借助Ecplise Memory Analyzer (MAT)分析dump文件&#xff0c;直接下载、安装、运行MAT报错 询问同事后&#xff0c;同事说可以先安装Ecplise&#xff0c;再以插件的形式安装MAT下载、安装好Eclipse&#xff0c;点击运行仍然报错&#xff0c;且错误信息一…