Linux线程(二)线程互斥

目录

一、为什么需要线程互斥

二、线程互斥的必要性

三、票务问题举例(多个线程并发的操作共享变量引发问题)

四、互斥锁的用法

1.互斥锁的原理

2、互斥锁的使用

1、初始化互斥锁

2、加锁和解锁

3、销毁互斥锁(动态分配时需要)

五、使用互斥锁改进票务问题

六、可重入与线程安全

 1、可重入(Reentrant)

2、线程安全(Thread Safety)


上篇文章我们讲解了线程的概念以及线程的基本操作:
Linux线程(一)初识线程

这篇文章我们来讲解一下线程互斥的内容。

一、为什么需要线程互斥

        当多个线程试图同时修改同一份数据时,可能会导致数据不一致、竞态条件等问题。

当两个或多个线程同时访问和修改同一个共享资源时,如果没有适当的同步控制,可能会导致数据处于不一致的状态。例如,一个线程正在读取某个变量的同时,另一个线程可能正在修改这个变量,最终结果可能既不是原始值也不是任何一个线程期望修改后的值,造成不可预料的行为。(后面会举例说明)

所以就引出了线程互斥 :
在Linux系统中,线程互斥是一种确保多个线程在访问共享资源时不会产生冲突的机制。这是通过使用互斥锁(Mutex)来实现的,它是防止并发执行线程同时进入临界区(即访问共享资源的代码段)的一种同步原语。

二、线程互斥的必要性

线程互斥是确保多线程环境下程序正确性、稳定性和可预测性的关键手段,通过限制对共享资源的同时访问,避免了并发执行可能引发的各种问题:

避免数据竞争(Data Race):当两个或多个线程同时访问和修改同一个共享资源时,如果没有适当的同步控制,可能会导致数据处于不一致的状态。例如,一个线程正在读取某个变量的同时,另一个线程可能正在修改这个变量,最终结果可能既不是原始值也不是任何一个线程期望修改后的值,造成不可预料的行为。

确保数据一致性:互斥机制确保了在任何时候,最多只有一个线程可以修改共享资源。这样可以保证每次对共享数据的修改都是完整且原子的,从而维护了数据的一致性。

预防竞态条件(Race Condition):竞态条件是指程序的输出依赖于非确定性的线程执行顺序。没有互斥锁,即使程序逻辑正确,由于线程调度的不确定性,也可能导致错误的结果。比如经典的“票务问题”,如果不使用互斥锁,多个线程同时减去票数可能会导致卖出超过实际存在的票数。

实现同步点:除了防止并发访问带来的问题,互斥锁还可以作为线程间的同步工具,用于控制线程执行的顺序。例如,一个线程可能需要等待另一个线程完成特定任务后才能继续执行。

保护资源的完整性:某些资源(如文件、数据库连接、硬件设备等)可能不支持同时访问,或者同时访问会导致错误或损坏。互斥锁确保这些资源在被一个线程使用时,其他线程不能访问,从而保护了资源的完整性。

三、票务问题举例(多个线程并发的操作共享变量引发问题)

我们来看以下代码,多个线程访问一个全局变量ticket来模拟抢票,ticket就是共享变量:

#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(1);
            printf("%s sells ticket:%d\n",id,ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,route,"thread 1");
    pthread_create(&t2,NULL,route,"thread 2");
    pthread_create(&t3,NULL,route,"thread 3");
    pthread_create(&t4,NULL,route,"thread 4");

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

运行后发现

 

票数竟然出现了0和-1,显然是不符合预期的。

每个线程在检查ticket变量是否大于1后,直接进行减操作和打印,没有确保在这两个操作之间没有其他线程也进行了同样的检查和操作。这导致了多个线程可能几乎同时判断ticket大于1,并都执行减1操作,造成票数卖超的错误。

多个线程直接读写共享变量ticket而没有加锁保护,这违反了线程安全原则。当一个线程正在读取ticket的值时,另一个线程可能正在修改它,导致读取到的是不一致或中间状态的数据。

-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
可能同时有几个线程判断了ticket>0,并进行了ticket--操作,但是这个时候ticket的值已经被其他线程修改,这个时候就造成了共享变量的数据错误。
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址

解决这些问题的关键是在访问共享资源(这里是ticket变量)之前使用互斥锁(Mutex),确保同一时间只有一个线程能执行临界区内的代码,从而避免了数据竞争和竞态条件,确保了线程安全。 

四、互斥锁的用法

1.互斥锁的原理

加锁(Lock):当一个线程想要进入临界区时,它会尝试获取互斥锁。如果锁未被其他线程持有,该线程将成功获取锁并进入临界区。

解锁(Unlock):完成对共享资源的操作后,线程会释放互斥锁,允许其他等待中的线程有机会获取锁并访问资源。

2、互斥锁的使用

在Linux中,使用POSIX线程库(pthread)来处理线程和互斥锁。以下是基本的使用步骤:

1、初始化互斥锁

静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数:
mutex:要初始化的互斥量
attr:NULL

2、加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况 :
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

3、销毁互斥锁(动态分配时需要)

使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

五、使用互斥锁改进票务问题

通过在访问和修改ticket变量前后分别调用pthread_mutex_lock()pthread_mutex_unlock(),确保了在任何时刻只有一个线程能进行售票操作,从而解决了线程间的数据竞争问题,保证了票数的准确减少,避免了超卖现象。

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.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--;
        }
        else
        {
            // 释放锁并跳出循环
            pthread_mutex_unlock(&mutex);
            break;
        }
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
   // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,route,"thread 1");
    pthread_create(&t2,NULL,route,"thread 2");
    pthread_create(&t3,NULL,route,"thread 3");
    pthread_create(&t4,NULL,route,"thread 4");

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

    // 最后记得销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行后发现保证了票数的准确减少,避免了超卖现象。

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作 , 大多数体系结构都提供了 swap exchange 指令 , 该指令的作用是把寄存器和内存单元的数据相交换, 由于只有一条指令 , 保证了原子性 , 即使是多处理器平台 , 访问内存的 总线周期也有先后 , 一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 

六、可重入与线程安全

 1、可重入(Reentrant)

定义:可重入指的是一个函数或一段代码可以在任意时刻被中断,然后再次进入并正确执行,即使在之前调用还未完成的情况下也是如此。对于可重入代码,最重要的是它的内部状态不会因多次调用而受损,且不依赖于外部状态或存储。

特点

  • 不使用静态或全局变量存储状态信息
  • 不使用用malloc或者new开辟出的空间
  • 如果必须使用全局数据,那么这些数据必须是只读的或能以线程安全的方式修改。
  • 函数不依赖于任何外部资源的状态,或能确保外部资源访问的线程安全性。
  • 递归调用是可重入的一个特例。

2、线程安全(Thread Safety)

定义:线程安全指多个线程同时访问(包括读取和写入)同一段代码或数据时,仍然能够保持正确的执行结果,不会引发数据不一致、崩溃或其他未定义行为。这意味着代码需要采取适当的同步措施(如互斥锁、信号量等)来防止数据竞争和竞态条件。

特点

  • 通过同步机制确保共享资源的访问是互斥的,防止数据竞争。
  • 可能通过加锁机制来实现,但这也会引入潜在的死锁和性能开销。
  • 线程安全的代码在多线程环境下不需要外部干预即可安全运行。

关系:

  • 交集可重入代码通常是线程安全的,因为它不依赖于全局状态,减少了并发访问的冲突点。
  • 区别并非所有线程安全的代码都是可重入的。例如,一个使用了锁来保护共享资源的函数,虽然线程安全(因为一次只有一个线程可以修改资源),但如果在锁内调用自己(递归调用),可能会导致死锁,因此不是可重入的。

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

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

相关文章

PostgreSQL的学习心得和知识总结(一百四十三)|深入理解PostgreSQL数据库之Support event trigger for logoff

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

Stable Diffusion教程|图生图原理和实战

Stable Diffusion凭借其卓越的图生图功能&#xff0c;极大地提升了图像生成的可控性与输出品质&#xff0c;赋予用户前所未有的个性化创作风格表达能力。这一革新特性使得Stable Diffusion不仅能精准地捕捉用户的艺术愿景&#xff0c;更能以数字化手段孕育出新颖且极具创意的画…

论文 学习 Transformer : Attention Is All You Need

目录 概述&#xff1a; 对摘要的理解&#xff1a; 框架解析 按比例缩放的点积注意力 多头注意力机制 前馈神经网络与位置编码 概述&#xff1a; transformer 是一个encoder ——decoder 结构的用于处理序列到序列转换任务的框架&#xff0c;是第一个完全依赖自注意力机制…

写了 1000 条 Prompt 之后,我总结出了这 9 个框架【建议收藏】

如果你对于写 Prompt 有点无从下手&#xff0c;那么&#xff0c;本文将为你带来 9 个快速编写 Prompt 的框架&#xff0c;你可以根据自己的需求&#xff0c;选择任意一个框架&#xff0c;填入指定的内容&#xff0c;即可以得到一段高效的 Prompt&#xff0c;让 LLM 给你准确满意…

再谈毕业论文设计投机取巧之IVR自动语音服务系统设计(信息与通信工程A+其实不难)

目录 举个IVR例子格局打开&#xff0c;万物皆能IVR IVR系统其实可盐可甜。还能可圈可点。 戎马一生&#xff0c;归来依然IVR。 举个IVR例子 以下是IVR系统的一个例子。 当您拨打电话进入IVR系统。 首先检验是否为工作时间。 如是&#xff0c;您将被送入ivr-lang阶段&#xff0…

python3如何安装bs4

在python官网找到beautifulsoup模块的下载页面&#xff0c;点击"downloap"将该模块的安装包下载到本地。 将该安装包解压&#xff0c;然后在打开cmd&#xff0c;并通过cmd进入到该安装包解压后的文件夹目录下。 在该文件目录下输入"python install setup.py&quo…

程序人生 | 人生如棋,落子无悔

人生的开始&#xff0c;始于哭声&#xff0c;浮浮沉沉几十年。终了&#xff0c;一声长叹&#xff0c;在一片哭声中撒手离去。 人生的道路虽然漫长&#xff0c;但是关键就是那么几次机会的选择&#xff0c;可以决定此后几十年的光阴。 有个故事讲&#xff1a;古代有个人去砍柴…

搭建一个Xx431?

搭建一个Xx431? 嘿uu们!刚结束了一周六天班感觉如何? 我的状态倒还行,工作生活总能找到乐子,本周整活就是用纸巾和蛋糕托做的油灯,另外想制冷片做个温水冷水可调的杯托,但我还不会搞3d,希望今年能搞起来. 题外话就说到这,这个选题也是因为实际遇到的问题需要这玩意,下班路…

基于Matplotlib的模型性能可视化工作

一、项目简介 本项目是科技考古墓葬识别工作的中间过程&#xff0c;因为需要大量复用所以另起一章好了。 主要涉及到数据读取、数据可视化和少量的数据处理过程。 二、相关知识 PandasMatplotlib 三、实验过程 1. 数据探索性分析 1.1 准备工作–导入模块 import pandas…

【Python系列】Python中列表属性提取

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

【Java orm 框架比较】十一 新增 原生jdbc对比

迁移到&#xff08;https://gitee.com/wujiawei1207537021/spring-orm-integration-compare&#xff09; orm框架使用性能比较 比较mybatis-plus、lazy、sqltoy、mybatis-flex、easy-query、mybatis-mp、jpa、dbvisitor、beetlsql、dream_orm、wood、hammer_sql_db、原生jdbc…

OpenCv中cv2.subtract(image,blurred)与(image-blurred)的区别

目录 一、cv2.subtract()函数二、cv2.subtract(image,blurred)和&#xff08;image-blurred&#xff09;处理效果对比2.1 代码2.2 输出结果 三、总结 一、cv2.subtract()函数 cv2.subtract是OpenCV库中的一个函数&#xff0c;用于进行图像减法运算。它可以很方便地进行两个图像…

LeetCode/NowCoder-链表经典算法OJ练习1

目录 说在前面 题目一&#xff1a;移除链表元素 题目二&#xff1a;反转链表 题目三&#xff1a;合并两个有序链表 题目四&#xff1a;链表的中间节点 SUMUP结尾 说在前面 dear朋友们大家好&#xff01;&#x1f496;&#x1f496;&#x1f496;数据结构的学习离不开刷题…

【C/C++笔试练习】DNS设置文件、应用层、Dos攻击、DNS服务、DNS、子网划分、http状态、路由设置、TCP连接、HTTP状态码、剪花布条、客似云来

文章目录 C/C笔试练习选择部分&#xff08;1&#xff09;DNS设置文件&#xff08;2&#xff09;应用层&#xff08;3&#xff09;Dos攻击&#xff08;4&#xff09;DNS服务&#xff08;5&#xff09;DNS&#xff08;6&#xff09;子网划分&#xff08;7&#xff09;http状态&am…

网络运维故障排错思路!!!!!(稳了!!!)

1 网络排错的必备条件 为什么要先讲必备条件&#xff1f;因为这里所讲的网络排错并不仅仅是停留在某一个小小命令的使用上&#xff0c;而是一套系统的方法&#xff0c;如果没有这些条件&#xff0c;我真的不能保证下面讲的这些你可以听得懂&#xff0c;并且能运用到实际当中&a…

Navicat 17:先睹为快

官方声明&#xff1a;Navicat 17&#xff08;英文版&#xff09;目前处于测试阶段中&#xff0c;并计划 5 月 13 日发布&#xff01; 如果你觉得 Navicat 16 已经推出很多令人兴奋的新功能&#xff0c;那么这次你可能要好好看看 Navicat 17&#xff0c;本次升级涵盖了更多的内容…

ASP.NET WebApi 如何使用 OAuth2.0 认证

前言 OAuth 2.0 是一种开放标准的授权框架&#xff0c;用于授权第三方应用程序访问受保护资源的流程。 OAuth 2.0 认证是指在这个框架下进行的身份验证和授权过程。 在 OAuth 2.0 认证中&#xff0c;涉及以下主要参与方&#xff1a; 资源所有者&#xff08;Resource Owner&…

【算法】动态规划之背包DP问题(2024.5.11)

前言&#xff1a; 本系列是学习了董晓老师所讲的知识点做的笔记 董晓算法的个人空间-董晓算法个人主页-哔哩哔哩视频 (bilibili.com) 动态规划系列 【算法】动态规划之线性DP问题-CSDN博客 01背包 步骤&#xff1a; 分析容量j与w[i]的关系&#xff0c;然后分析是否要放…

iLogtail 社区开源之夏活动来了!

作者&#xff1a;玄飏 在这个充满活力的夏日&#xff0c;随着阳光一同灿烂的是开源精神的光辉与创新的火花。iLogtail 社区高兴地宣布&#xff0c;我们正式加入开源之夏 2024 的行列&#xff0c;诚邀每一位怀揣梦想与激情的学生开发者&#xff0c;共同开启一场探索技术前沿、贡…

WP All Import Pro插件下载 - 一键导入,无限可能

在当今快节奏的数字时代&#xff0c;网站内容的更新和管理是每个网站管理员和开发者的日常工作。但是&#xff0c;传统的手动更新方法不仅耗时&#xff0c;而且容易出错。现在&#xff0c;有了WP All Import Pro&#xff0c;这一切都将改变。 WP All Import Pro 是一款专为Wor…