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

文章目录

  • 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开辟出的空间
  • 不调用不可重入函数

可重入与线程安全联系

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

中霖教育怎么样?中级经济师证书领取流程介绍

在成功通过中级经济师考试之后&#xff0c;需要等待约2到3个月的时间&#xff0c;会发布相关领证公告。 在准备材料方面&#xff0c;考生需确保携带以下文件&#xff1a;身份证件、学历证明以及工作年限的证明文件等&#xff0c;前往相应的人事考试局进行资格审核。 资格审核…

探索交通安全新前沿:构建全面精准的交通事故数据集(目标检测)

亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 在21世…

Redis基础教程(十八):Redis管道技术

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

tomcat 安装和优化

tomcatat tomcat和http一样&#xff0c;都是用来处理动态页面的 tomcat也可以作为web服务器&#xff0c;开源的 php.php tomcat.jsp nginx.html tomcat使用java代码写的程序&#xff0c;运行的是java的web服务程序 tomcat的特点和功能&#xff1a; 1、servlet容器&…

【硬核科普】Ubuntu系统详细解析以及与深度学习的关系

文章目录 0. 前言1. Ubuntu的来源1.1 从Linux说起1.2 开源、稳定的Debian1.3 更稳定、友好且开放的Ubuntu 2. Ubuntu与深度学习3. Ubuntu在自动驾驶领域的应用4. 附录&#xff1a;Linux发行版统计 0. 前言 按照国际惯例&#xff0c;首先声明&#xff1a;本文只是我自己学习的理…

白蛇插画:成都亚恒丰创教育科技有限公司

白蛇插画&#xff1a;古韵今风&#xff0c;情深意长 在浩瀚的艺术长河中&#xff0c;插画作为一种独特的艺术形式&#xff0c;以其生动形象的画面、丰富多彩的色彩和深邃悠远的意境&#xff0c;成都亚恒丰创教育科技有限公司深受人们喜爱。而“白蛇插画”&#xff0c;作为融合…

图像识别和目标检测在超市电子秤上的应用

目录 前言深度学习的目标检测图像识别技术视觉秤的优势其他应用场景中的技术应用未来展望 前言 随着科技的不断发展&#xff0c;电子秤在生鲜超市中的应用也在不断升级。传统的电子秤需要打秤人员手动输入秤码&#xff0c;这不仅耗时费力&#xff0c;还需要大量的培训以记住各…

Zabbix 6.0 组件 工作原理和组件

Zabbix组件 C/S架构 服务端&#xff1a;zabbix server&#xff08;端口10051&#xff09;&#xff1a;zabbix服务端进程&#xff0c;用于接收代理端发来的监控指标数据&#xff0c;配置和管理zabbix应用程序&#xff0c;也是监控系统的告警中心&#xff08;通过监控项告警触…

将亚马逊甩在身后的Flipkart在印度加速狂奔,Flipkart中国卖家怎么入驻开店?

Flipkart创立于2007年&#xff0c;是印度第一大电商平台&#xff0c;发展势头可谓十分迅猛&#xff0c;在印度就连亚马逊都被它远远地甩在了身后。除了销售图书和电子产品&#xff0c;Flipkart还有一个在线市场&#xff0c;允许第三方卖家入驻销售产品。 将亚马逊甩在身后的Fli…

基于全国产复旦微JFM7K325T+ARM人工智能数据处理平台

复旦微可以配合的ARM平台有&#xff1a;RK3588/TI AM62X/ NXP IMX.8P/飞腾FT2000等。 产品概述 基于PCIE总线架构的高性能数据预处理FMC载板&#xff0c;板卡采用复旦微的JFM7K325T FPGA作为实时处理器&#xff0c;实现各个接口之间的互联。该板卡可以实现100%国产化。 板卡具…

ArkUI开发学习随机——B站视频简介页面,美团购买界面

案例一&#xff1a;B站视频简介页面 代码&#xff1a; build() {Column(){Column(){Stack(){Image($r("app.media.genimpact")).width(200).height(125).borderRadius({topLeft:5,topRight:5})Row(){Image($r("app.media.bz_play")).height(24).fillColor…

游戏AI的创造思路-技术基础-决策树(1)

决策树&#xff0c;是每个游戏人必须要掌握的游戏AI构建技术&#xff0c;难度小&#xff0c;速度快&#xff0c;结果直观&#xff0c;本篇将对决策树进行小小解读~~~~ 目录 1. 定义 2. 发展历史 3. 决策树的算法公式和函数 3.1. 信息增益&#xff08;Information Gain&…

阿尔泰科技与西安交通大学陕西省某技术重点实验室共谋未来!

近日&#xff0c;阿尔泰科技的电子工程师&#xff08;熊工&#xff09;应邀前往西安交通大学陕西省某技术重点实验室&#xff0c;参与课题组项目的测试与调试工作。此次合作不仅成功推动了项目的进展&#xff0c;还为未来的深入合作奠定了坚实基础。 阿尔泰科技作为领先的测控技…

本地部署 Llama3 – 8B/70B 大模型!

Llama3&#xff0c;作为Meta公司新发布的大型语言模型&#xff0c;在人工智能领域引起了广泛的关注。特别是其8B&#xff08;80亿参数&#xff09;版本&#xff0c;在性能上已经超越了GPT-3.5&#xff0c;而且由于是开源的&#xff0c;用户可以在自己的电脑上进行部署。 本文和…

AI in Healthcare 医疗领域AI应用-基于DeepNLP AI App Store 真实用户评论打分和排名

website: Best AI in Healthcare DeepNLP AI Store github: https://rockingdingo.github.io/ai_store 医疗健康(AI in Healthcare)领域哪些AI服务应用更能满足用户的需求&#xff0c;排名最高? 参考deepnlp.org网站根据用户真实评论打分和show case分享&#xff0c;分为下列…

光敏电阻,光敏三极管,光敏二极管的作用与区别

光敏电阻、光敏三极管和光敏二极管的作用与区别 光敏电阻(Photocell/Photoresistor) 作用: 光敏电阻是一种对光敏感的电阻器,当光线照射到它时,其电阻会发生变化。它的主要作用包括: 光感应:用来检测环境光强度的变化。光控开关:在自动化控制中,根据光强变化实现开关…

商家转账到零钱分销返佣场景驳回处理办法

在处理商家转账到零钱分销返佣场景被驳回的问题时&#xff0c;商家需要了解驳回的原因&#xff0c;并采取相应的措施来解决这些问题。下面将详细介绍几种常见的驳回原因以及应对策略&#xff1a; 1. 多级分销模式问题 • 原因&#xff1a;如果业务模式涉及多级分销&#xff0c;…

win10的cmd窗口打开中文乱码

在pycharm中的terminal中同样是乱码&#xff0c;因为他们用的都是windows的窗口。 这里我们首先看我们打开的文件的什么编码&#xff0c;可以用notepad打开。 可以看到是utf-8编码 然后我们在cmd中或者Pycharm的terminal中 输入(这个就是Utf8编码) chcp 65001 然后在用type …

【前端】面试八股文——meta标签

【前端】面试八股文——meta标签 在HTML文档中&#xff0c;meta标签是一个关键但常被忽视的元素。它位于文档的<head>部分&#xff0c;用于提供关于HTML文档的元数据&#xff08;metadata&#xff09;。这些元数据不会直接显示在页面上&#xff0c;但对搜索引擎优化&…

10款超好用的文档加密软件丨2024文档加密软件分享

在现代的信息社会&#xff0c;企业和个人的重要数据都存储于电脑中&#xff0c;一旦丢失后果不堪设想。因此&#xff0c;文档加密软件应运而生。 文档加密软件是一种用于保护电子文档安全性的工具&#xff0c;它通过加密技术对文档内容进行编码&#xff0c;使得未授权的用户无…