【线程安全】线程互斥的原理

news2024/11/15 17:48:54

文章目录

  • Linux线程互斥
    • 线程互斥相关概念
    • 互斥量mutex
      • 引出线程并发问题
      • 引出互斥锁、互斥量
    • 互斥量的接口
      • 初始化互斥量
      • 销毁互斥量
      • 互斥量加锁和解锁
      • 使用互斥锁抢票
  • 可重入和线程安全
    • 概念:
    • 常见线程不安全的情况
    • 常见线程安全的情况
    • 常见不可重入的情况
    • 常见可重入情况
    • 可重入与线程安全联系

Linux线程互斥

线程互斥(Mutual Exclusion)是多线程编程中的一个重要概念,用于解决多个线程同时访问共享资源时可能产生的竞争条件(Race Condition)和数据不一致问题

在深入探索Linux线程互斥之前先了解一些基本概念

线程互斥相关概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部访问临界资源的代码片段,就叫做临界区
  • 互斥:任何时刻,互斥保证只有一个执行流进入临界区,访问临界资源,是保障临界资源安全的重要手段
  • 原子性:跟MySQL中事务的原子性一样,如果某种操作只有要么完成要么没完成两种状态,我们就称这种操作是原子性的。在这里原子性保证线程的某种操作不会被任何调度机制打断
  • 竞争条件:多个线程一起访问同一个共享资源,可能会导致不确定的结果。线程之间的这种关系就成为竞争,这种现象就称为竞争条件。

互斥量mutex

下面来看一个经典的抢票样例,代码如下:

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;

void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return arg;
}

int main(void)
{
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

全局变量ticket作为临界资源被多个线程同时访问,看看代码的运行结果:

  • 结果片段1
    在这里插入图片描述
  • 结果片段二
    在这里插入图片描述

引出线程并发问题

根据以上代码,我们提出以下疑问:

  1. 为什么多个线程会抢到同一张票
  2. 为什么票数会出现负数

我们来看上面的代码中的核心片段:

if (ticket > 0)
{
  usleep(1000);
  printf("%s sells ticket:%d\n", id, ticket);
  ticket--;
}
  • 在if判断结束之后,代码可以并发的切换到其它的线程,这样一来,很有可能当前线程还没有执行ticket--这个操作,其它线程就进来了。这就是为什么会有多个线程抢到同一张票的原因。
  • 同样的,多个线程抢到同一张票,并依次执行ticket--,最终出现票数为负数的情况。简单来说就是,if判断ticket--,两个操作中间并不是“无缝的”。
  • ticket-- 操作本身就不是一个原子操作,--操作并非是一条汇编语句,而是三条汇编语句的集合。
    • load :将共享变量ticket从内存加载到寄存器中
    • update : 更新寄存器里面的值,执行-1操作
    • store :将新值,从寄存器写回共享变量ticket的内存地址

要解决上面线程并发带来的问题需要做到以下几点:

  • 线程访问临界区必须是互斥的,即一个线程访问临界区时其它线程阻塞
  • 如果线程没有访问临界区,那么该线程不能阻止其它线程进入临界区。

引出互斥锁、互斥量

要想做到一个线程访问临界区时阻止其它线程访问,我们就需要使用到互斥锁机制。锁,顾名思义就是锁上临界区不让其它线程进去。而实现互斥锁机制就需要用到互斥量。这里需要注意互斥锁和互斥量两个名词的区分:

  • 互斥量是一个数据结构或对象,通常包含了锁的状态信息,是否被锁定等
  • 互斥锁是互斥量的操作机制,通过互斥锁操作,线程可以在进入临界区之前锁定互斥量,退出临界区时解锁互斥量。这也对应着我们常说的上锁和解锁两个操作。

为了方便阐述,后面出现的锁和互斥量不做过多区分。

在这里插入图片描述
上图中表示使用互斥锁实现线程互斥的示意图,lock表示上锁(),上锁之后其他线程不能访问临界区,unlock表示解锁,解锁之后其他线程才可以继续申请上锁。下面解释上锁是原子操作的原理:

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
在这里插入图片描述

加锁的具体步骤:

  1. 初始化互斥锁,初始化通常在声明时完成

  2. 申请获取锁

    • 检查锁的状态,如果是空闲状态则继续执行锁操作,否则就阻塞等待
    • 在汇编层面上使用xchgb指令(作用和swap一样),交换寄存器和内存中的互斥量状态数据,假设置1,使得申请到锁之后该线程能访问临界资源。而由于此时内存中互斥量的状态为0,其它线程无法申请锁就失败了
  3. 访问临界区

  4. 释放锁

互斥量的接口

初始化互斥量

  • 静态分配:
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 动态分配:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

其中pthread_mutex_t *mutex:指向需要初始化的互斥锁对象的指针。
const pthread_mutexattr_t *attr:指向互斥锁属性对象的指针。如果传入NULL,则使用默认属性。

销毁互斥量

如果要销毁一个互斥量可以执行以下代码:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex)

注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁(在栈上分配,自动销毁)
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁

对某个互斥量进行加锁或者解锁,成功返回0,失败返回错误码
具体的,调用pthread_lock时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁住
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

使用互斥锁抢票

了解了互斥的原理以及相关的操作之后,我们可以对上面的样例代码做出以下改进:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

int ticket = 100;
pthread_mutex_t mutex;//声明互斥量

void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&mutex);//访问临界区之前申请锁
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);//访问结束之后释放锁
            // sched_yield(); 放弃CPU
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return arg;
}

int main(void)
{
    pthread_t t1, t2, t3, t4;

    pthread_mutex_init(&mutex, NULL);//初始化互斥量

    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);//主动销毁互斥量
}

观察结果:
在这里插入图片描述
在这里插入图片描述

可重入和线程安全

概念:

  • 线程安全问题:多个线程访问同一段代码的时候出现数据不一致问题。常见对全局变量或者是静态变量进行操作,并且没有锁保护的情况就会出现线程安全问题
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他执行流再次进入(线程并发执行导致),我们称这种现象叫重入。一个函数在重入的情况下,运行结果不会出现任何数据不一致问题或者是其他问题,则该函数就被称为可重入函数。否则就是不可重入函数

常见线程不安全的情况

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

常见线程安全的情况

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

常见不可重入的情况

-== 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的==

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数不可重入不代表一定会引发线程安全问题
  • 如果一个函数中有全局变量且尝试修改,那么这个函数既不是可重入,也不是线程安全的
  • 可重入函数是线程安全函数的一种

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

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

相关文章

jvm 05JVM - 对象的创建 ,oop模型,字符串常量池

01.JVM - 对象的创建 1、对象的创建的方式 Java语言中&#xff0c;对象创建的方式有六种&#xff1a; new关键字&#xff1a;最常见的形式、Xxx的静态方法、XxxBuilder、XxxFactory的静态方法。 Class类的newInstance()方法&#xff1a;通过反射的方式创建对象&#xff0c;调…

92. 反转链表 II (Swift 版本)

题目描述 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 分析 这是一个经典的链表问题&#xff0c;要求反转链表的部分节点。我们可以通过以下步骤实…

Codesys 连接 EtherCAT 总线伺服

本文内容是根据参考视频做的笔记&#xff1a; EtherCAT Master 控制&#xff1a;https://www.bilibili.com/video/BV1L14y1t7ks/EtherCAT Master Motion 控制&#xff1a;https://www.bilibili.com/video/BV16P411j71E/ EtherCAT 总线简单介绍 从站站号&#xff1a;如果使用扫…

【pytorch22】激活函数与GPU加速

激活函数 ReLu还是还是可能出现梯度弥散&#xff0c;因为x<0的时候&#xff0c;梯度还是可能小于0 leaky relu&#xff0c;在x<0的时候&#xff0c;梯度就不会为0&#xff0c;梯度不会不动 Relu函数在x0处是不连续的 一种更加光滑的曲线是SELU&#xff0c;是两个函数的…

ESXI6.7升级补丁报错VIB QLC_bootbank_qedrntv

1、报错如下图 2、原因 VMware在下方链接说的很清楚&#xff0c;报错原因为OEM提供的镜像与新版本补丁某些驱动不兼容&#xff1b; https://knowledge.broadcom.com/external/article?legacyId78487https://knowledge.broadcom.com/external/article?legacyId78487 3、解决 …

自动化立体仓库设计步骤:7步

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 完整版文件和更多学习资料&#xff0c;请球友到知识星球【智能仓储物流技术研习社】自行下载 这份文件是关于自动化立体仓库设计步骤的详细指南&#xff0c;其核心内容包括以下几个阶…

Git常用技能速成

文章目录 一.版本控制二.提交并推送代码三.提交推送代码 一.版本控制 接下来&#xff0c;我们就需要对我们的功能进行优化&#xff0c;但是需要说明的是&#xff0c;我们不仅仅要对上述提到的缓存进行优化&#xff0c;还需要对我们程序的各个方面进行优化。我们本章节主要是针…

mirthConnect 常用示例和语法整理

mirthConnect 常用示例和语法整理 1、jolt json常用语法 https://please.blog.csdn.net/article/details/140137463 2、常用方法 2.1 WinningDateUtils 所有的时间工具在WinningDateUtils里面 获取当前时间&#xff1a;var nowStrWinningDateUtils.getStandardNowStr()获取…

【C++】开源:格式化库fmt配置与使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍格式化库fmt配置与使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下…

Android 通知访问权限

问题背景 客户反馈手机扫描三方运动手表&#xff0c;下载app安装后&#xff0c;通知访问权限打不开。 点击提示“受限设置” “出于安全考虑&#xff0c;此设置目前不可用”。 问题分析 1、setting界面搜“授予通知访问权限”&#xff0c;此按钮灰色不可点击&#xff0c;点…

Linux系统下anaconda的安装与Pytorch环境的下载

首先&#xff0c;在命令行通过cd命令&#xff0c;进入用户文件夹 cd xxx/xxx/username进入anaconda官网https://repo.anaconda.com/archive/&#xff0c;寻找anaconda下载包资源&#xff0c;这里选择最新的anaconda下载包 Anaconda3-2024.06-1-Linux-x86_64.sh 在命令行执行安…

项目收获总结--Redis的知识收获

一、概述 最近几天公司项目开发上线完成&#xff0c;做个收获总结吧~ 今天记录Redis的收获和提升。 二、Redis异步队列 Redis做异步队列一般使用 list 结构作为队列&#xff0c;rpush 生产消息&#xff0c;lpop 消费消息。当 lpop 没有消息的时候&#xff0c;要适当sleep再…

土壤检测仪器:精确地检测土壤元素

在农业生产的广阔天地中&#xff0c;土壤检测仪器如同一把钥匙&#xff0c;打开了我们认识土壤、了解土壤元素的大门。这些看似平凡却功能强大的设备&#xff0c;能够精确地检测出土壤中的各种元素&#xff0c;为农业生产提供科学、准确的数据支持。 一、土壤检测仪器的重要性 …

大气热力学(5)——绝热过程

本篇文章源自我在 2021 年暑假自学大气物理相关知识时手写的笔记&#xff0c;现转化为电子版本以作存档。相较于手写笔记&#xff0c;电子版的部分内容有补充和修改。笔记内容大部分为公式的推导过程。 文章目录 5.1 气块的概念5.2 热力学第一定律的几种微分形式5.3 干绝热过程…

为什么要进行学术会议投稿?

为什么要进行学术会议投稿&#xff1f; 学术会议投稿有以下几个重要的用途&#xff1a; 学术会议投稿有什么用 1. 学术交流与分享&#xff1a;学术会议是学者们交流和分享最新研究成果、观点和发现的平台。通过投稿并获得口头或海报展示的机会&#xff0c;您可以向其他学者介…

网络祭祀人物微信小程序模板源码

模板介绍 手机端网络祭祀&#xff0c;在线祭祀&#xff0c;创建纪念历史人物小程序前端模板下载。包含&#xff1a;人物列表、详情、创建人物、个人中心等等页面。 图片演示 网络祭祀人物微信小程序模板源码

【Kubernetes安装】从零开始使用kubeadm命令工具部署K8S v1.28.2 集群

文章目录 一、虚拟机配置参数说明二、kubernetes v1.28.2版本介绍三、CentOS 7.9 系统初始化配置3.1 配置CentOS系统基础环境3.1.1 配置hosts3.1.2 永久关闭selinux3.1.3 关闭swap分区3.1.4 所有节点全部关闭防火墙3.1.5 配置ntp server同步时间3.1.6 添加kubernetes镜像源 3.2…

代码随想录算法训练营第二天|【数组】59.螺旋矩阵II

这两天工作的事情有点多&#xff0c;周末又比较懒&#xff0c;所以没有跟上进度。这两天开始补进度。 题目 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&a…

centos7升级到欧拉openeule

centos7升级到欧拉openeule 一、准备工作 1、安装迁移工具&#xff08;安装迁移工具的机器不能给自己升级&#xff0c;请用其他机器作为迁移母机&#xff09; wget https://repo.oepkgs.net/openEuler/rpm/openEuler-20.03-LTS-SP1/contrib/x2openEuler/x86_64/Packages/x2…

使用webrtc-streamer查看rtsp实时视频

1.下载webrtc-streamer 2.解压运行webrtc-streamer.exe 在浏览器访问127.0.0.1:8000&#xff0c;点击窗口可以看到本机上各窗口实时状态&#xff0c;点击摄像头可以显示摄像头画面。 5.安装phpstudy&#xff0c;并建立网站。&#xff08;具体过程自己网上搜&#xff09; 6.打开…