探索Linux互斥:线程安全与资源共享

个人主页:chian-ocean

文章专栏-Linux

前言:

互斥是并发编程中避免竞争条件和保护共享资源的核心技术。通过使用锁或信号量等机制,能够确保多线程或多进程环境下对共享资源的安全访问,避免数据不一致、死锁等问题。

在这里插入图片描述

竞争条件

竞争条件(Race Condition)是并发程序设计中的一个问题,指在多个线程或进程并发执行时,由于它们对共享资源的访问顺序不确定,可能导致程序的输出或行为依赖于执行的顺序,从而产生不一致或不可预测的结果。

例如:一个假脱机打印程序。当一个进程需要打印一个文件时,它将文件名放在一个特殊的假脱机目录**(spoalerdirectory)**下。另一个进程(打印机守护进程)则周期性地检查是否有文件需要打印,若有就打印并将该文件名从目录下删掉)

在这里插入图片描述

  • 理想情况

    1. 设想假脱机目录中有许多槽位,编号依次为0,1,2,……,每个槽位存放一个文件名。
    2. 同时假设有两个共享变量:out,指向下一个要打印的文件:in,指向目录中下一个空闲槽位。
    3. 可以把这两个变量保存在一个所有进程都out=4进程Ain=7能访问的文件中,该文件的长度为两个字。
    4. 在某一时刻,进程B号至3号槽位空(其中的文件已经打印完毕),4号至6号槽位被占用(其中存有排好队列的要打印的文件名)。几乎在同时刻,进程A进程B都决定将一个文件排队打印,这种情图两个进程同时想访问共享内存
  • 实际情况

    1. 进程A读到 in 的值为7,将7存在一个局部变量 next_free_slot中。
    2. 此时发生一次时钟中断,CPU认为进程A已运行了足够长的时间,决定切换到进程B。进程B也读取in,同样得到值为7,于是将7存在B的局部变量next_free_slot中。
    3. 在这一时刻两个进程都认为下一个可用槽位是7.进程B现在继续运行,它将其文件名存在槽位7中并将in的值更新为8。然后它离开,继续执行其他操作最后进程A接着从上次中断的地方再次运行。
    4. 它检查变量 next_free_slot,发现其值为7,于是将打印文作名存人7号槽位,这样就把进程B存在那里的文件名覆盖掉。然后它将 next_free_slot加1,得到值为8,就将8存到in中。
    5. 此时,假脱机目录内部是一致的,所以打印机守护进程发现不了任何错误,但进程B却永远得不到任何打印输出。类似这样的情况,即两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件(race condition)。

实际抢票问题

#include<iostream>
#include<unistd.h>
#include<pthread.h>using namespace std;#define NUM  10 // 定义线程数量,这里创建 10 个线程
int ticket = 1000; // 票数从 1000 开始// 线程执行的函数
void* mythread(void* args)
{pthread_detach(pthread_self());  // 分离线程,线程结束后自动释放资源uint64_t number = (uint64_t)args;  // 将传入的参数(线程编号)转换为 uint64_t 类型while(true){if(ticket > 0) // 如果还有票{usleep(1000); // 模拟一些延迟,减少系统负载cout <<"thread: " << number << " ticket: " << ticket << endl; // 打印线程编号和剩余票数ticket--;  // 减少票数}else {break; // 如果没有票了,退出循环}usleep(20);  // 再次暂停 20 微秒,模拟其他操作}return nullptr; // 线程结束时返回空指针
}int main()
{// 创建 NUM 个线程for(int i = 0; i < NUM; i++){pthread_t tid;pthread_create(&tid,nullptr,mythread,(void*)i);  // 创建线程,传入线程编号}sleep(5); // 主线程等待 5 秒,确保子线程有足够的时间执行cout <<"process quit ..." <<endl;  // 打印主线程退出消息return 0;
}

简单描述:

  1. 线程数量和票数
    • 定义了一个全局变量 ticket,初始值为 1000,表示共有 1000 张票。
    • 程序创建了 10 个线程(NUM = 10),每个线程将尝试减少 ticket 的值,模拟每个线程购买一张票。
  2. 线程函数
    • 每个线程执行 mythread 函数,函数内部通过一个 while 循环不断检查 ticket 是否大于 0。如果 ticket 大于 0,则线程会输出剩余票数并减去一张票,模拟卖票操作。
    • 使用 usleep(1000) 模拟了一个小延迟,避免线程占用过多 CPU 资源,并且增加了另一个小的 usleep(20) 让线程执行有一定的间隔。
  3. 主线程
    • 主线程创建了 10 个线程,并且等待 5 秒后退出,给子线程一些时间执行任务。

在这里插入图片描述

潜在问题:

  1. 竞态条件(Race Condition)
    • 问题描述:多个线程同时访问并修改共享资源 ticket,可能会发生竞态条件。由于 ticket-- 操作并不是原子的(即分为读取、修改和写入三步),多个线程在同一时间访问 ticket 时,可能会同时读取到相同的值并同时更新,导致票数没有正确减少,可能会出现卖出同一张票的情况。
    • 解决方案:可以通过互斥锁(pthread_mutex_t)来保证每次只有一个线程能修改 ticket,避免并发写入导致的错误。

临界区

临界区(Critical Section) 是指在多线程或多进程程序中,共享资源被多个线程或进程同时访问和修改的代码区域。为了确保共享资源在多线程或多进程环境中的一致性和正确性,我们需要对访问临界区的操作进行同步控制,以避免发生竞争条件(Race Condition)。

临界区的特点:

  1. 共享资源访问:临界区中的代码通常会访问共享资源,例如共享内存、文件、全局变量、硬件资源等。
  2. 并发执行:多个线程或进程可能同时尝试进入临界区,并对共享资源进行修改。
  3. 资源竞争:如果多个线程/进程在同一时刻进入临界区并修改共享资源,就可能导致数据冲突、不一致或错误。

临界区的问题:

  • 数据一致性问题:多个线程或进程同时修改共享数据,可能导致数据不一致、错误或丢失。
  • 资源冲突:当多个线程或进程试图同时访问共享资源时,可能会引发系统资源竞争,影响程序的正确性和效率。

解决方案

  • 互斥锁(Mutex): 互斥锁用于确保在某一时刻只有一个线程能够访问临界区。当一个线程需要进入临界区时,它会获取互斥锁,其他线程必须等待该线程释放锁后才能进入临界区。
  • 信号量(Semaphore): 信号量可以控制对共享资源的并发访问。通过限制允许访问临界区的线程数量,可以避免过多的线程同时进入临界区。

这样尽管可以避免竞争条件,但是这样不能保证共享数据进行正确高效的协作,还要满足以下4个条件:

  1. 任何两个进程不能同时处于临界区。
  2. 不应该对CPU的数量和速度进行任何假设。
  3. 临界区外的进程不得阻塞其他进程。
  4. 不得使进程无期限等待进入临界区。

临界区的优化:

  1. 减少临界区的长度:尽量将临界区的代码量减少到最小,避免过长时间占用临界区。
  2. 避免不必要的锁:对于只读的共享资源,尽量避免加锁,减少锁带来的性能开销。
  3. 使用无锁编程(Lock-Free Programming):通过原子操作(如 atomic 类型)和 CAS(Compare-And-Swap)等无锁技术,避免传统锁机制带来的性能瓶颈。

互斥锁

互斥锁(Mutex) 是一种用于多线程编程的同步机制,旨在防止多个线程同时访问和修改共享资源,从而确保数据的一致性和程序的正确性。

互斥锁初始化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 全局域初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 使用默认属性初始化
  • 局部区初始化
pthread_mutex_t mutex;  // 定义一个互斥锁变量pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁。NULL表示使用默认的属性pthread_mutex_destroy(&mutex);  // 销毁互斥锁,在不再使用锁时调用

加锁、解锁

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • pthread_mutex_lock:用于锁定一个互斥锁。若互斥锁已被其他线程锁定,则调用线程会阻塞,直到互斥锁被释放。

  • pthread_mutex_trylock:尝试锁定互斥锁。与 pthread_mutex_lock 不同的是,它不会阻塞线程。如果锁定成功,返回 0;如果锁定失败(即锁已经被其他线程持有),则返回一个非零值。

  • pthread_mutex_unlock:用于解锁一个已锁定的互斥锁。如果当前线程没有持有该锁,调用此函数将导致未定义的行为。

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁pthread_mutex_lock(&mutex);  // 锁定互斥锁
// 访问共享资源
pthread_mutex_unlock(&mutex);  // 解锁互斥锁

优化抢票问题

#include<iostream>   
#include<unistd.h>   
#include<pthread.h>  
using namespace std;pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;  // 定义并初始化一个互斥锁
#define NUM  10  // 定义创建的线程数量
int ticket = 1000;  // 定义一个全局变量 ticket,初始为 1000,表示票的数量// 线程函数,用于模拟每个线程购买票
void* mythread(void* args)
{pthread_detach(pthread_self());  // 将当前线程设置为分离线程,结束后自动回收资源uint64_t number = (uint64_t)args;  // 将传入的参数(线程编号)转换为 uint64_t 类型while(true)  // 循环,直到票数为 0{{pthread_mutex_lock(&lock);  // 锁定互斥锁,确保对 ticket 资源的互斥访问if(ticket > 0)  // 如果还有票{usleep(1000);  // 模拟工作延迟,单位为微秒(1 毫秒)cout <<"thread: " << number << " ticket: " << ticket << endl;  // 输出当前线程编号和剩余票数ticket--;  // 票数减少}else  // 如果票数为 0,退出循环{break;}pthread_mutex_unlock(&lock);  // 解锁,允许其他线程访问 ticket 资源}}return nullptr;  // 返回空指针,结束线程
}int main()
{// 创建多个线程for(int i = 0; i < NUM; i++)  // 创建 NUM 个线程{pthread_t tid;  // 定义线程 IDpthread_create(&tid, nullptr, mythread, (void*)i);  // 创建线程并传递参数(线程编号)}sleep(5);  // 主线程休眠 5 秒,确保所有线程执行一段时间cout <<"process quit ..." <<endl;  // 输出退出信息,表示主进程结束return 0;  // 返回 0,程序结束
}

代码详细注释解析:

  1. 全局变量

    • pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;:定义了一个全局互斥锁,初始化时就已经可用。这个锁是为了防止多个线程同时访问和修改 ticket 变量导致的并发问题。
    • #define NUM 10:定义了一个宏 NUM,表示需要创建的线程数量(此处为 10)。
    • int ticket = 1000;:全局变量 ticket 表示剩余票数,初始值为 1000。
  2. 线程函数 mythread

    • pthread_detach(pthread_self());:将当前线程设置为分离线程,这样线程结束时系统会自动回收资源,无需显式调用 pthread_join 来等待线程结束。

    • uint64_t number = (uint64_t)args;:将传递给线程函数的参数(线程编号)转换为 uint64_t 类型,以便进行打印。

    • while(true)循环中,线程将不断检查 ticket

      是否大于 0:

      • 使用 pthread_mutex_lock(&lock); 上锁,防止多个线程同时修改 ticket 变量,保证每次只有一个线程能访问和修改票数。
      • 如果 ticket > 0,则输出当前线程的编号和剩余票数,并将票数减 1。每次操作后调用 usleep(1000); 来模拟工作延时。
      • 如果 ticket 为 0,跳出循环。
      • 最后,通过 pthread_mutex_unlock(&lock); 解锁,允许其他线程访问共享资源。
  3. 主函数 main

    • for 循环中,创建了 10 个线程,每个线程都会执行 mythread 函数。线程编号(i)被传递到每个线程中,作为其唯一标识。
    • sleep(5);:主线程休眠 5 秒,以确保创建的 10 个子线程有足够的时间执行完毕。
    • cout <<"process quit ..." <<endl;:输出程序退出信息,表示主程序结束。

打印

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

互斥锁封装(RAII)

class mutex
{
public:
private:pthread_mutex_t * _mutex;  // 互斥锁指针public:mutex(pthread_mutex_t* mutex):_mutex(mutex)  // 构造函数,初始化互斥锁指针{//pthread_mutex_init(_mutex,nullptr);  // 互斥锁的初始化被注释掉了}void lock(){pthread_mutex_lock(_mutex);  // 锁定互斥锁}void unlock(){pthread_mutex_unlock(_mutex);  // 解锁互斥锁}~mutex() {}  // 析构函数,什么都不做
};class Guard
{
private:mutex _lock;  // 使用上面定义的 mutex 类来管理锁public:Guard(pthread_mutex_t* lock):_lock(lock)  // 构造函数中锁定互斥锁{_lock.lock();  // 自动锁定}~Guard()  // 析构函数中解锁{_lock.unlock();  // 自动解锁}
};

这段代码的设计实现了一个典型的 RAII(资源获取即初始化) 模式,尤其是在 Guard 类中得到了完美的体现。RAII 是 C++ 中管理资源(如内存、文件句柄、互斥锁等)的一种设计模式。在该模式下,资源在对象的构造函数中获取,在对象的析构函数中释放,这样可以确保即使发生异常,也能正确释放资源,避免资源泄漏和死锁。

// 构造函数中锁定互斥锁
{
_lock.lock(); // 自动锁定
}

~Guard()  // 析构函数中解锁
{_lock.unlock();  // 自动解锁
}

};


这段代码的设计实现了一个典型的 **RAII(资源获取即初始化)** 模式,尤其是在 `Guard` 类中得到了完美的体现。RAII 是 C++ 中管理资源(如内存、文件句柄、互斥锁等)的一种设计模式。在该模式下,资源在对象的构造函数中获取,在对象的析构函数中释放,这样可以确保即使发生异常,也能正确释放资源,避免资源泄漏和死锁。

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

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

相关文章

《Stable Diffusion 3.0企业级落地指南》——技术赋能与商业价值的深度融合实践

Stable Diffusion 3.0&#xff08;SD3&#xff09;作为当前多模态生成式AI技术的集大成者&#xff0c;凭借其创新的扩散Transformer架构&#xff08;DiT&#xff09;、流匹配&#xff08;Flow Matching&#xff09;技术以及超分辨率生成能力&#xff0c;正在重塑企业内容生产的…

基于本地模型+多级校验设计的高效缓存,有效节省token数量(有点鸡肋doge)。

前言 我是基于token有限而考虑的一个省钱方案&#xff0c;还能够快速返回结果&#xff0c;但是劣势也很明显&#xff0c;设计不好容易出问题&#xff0c;就如下面所介绍的语义飘逸和缓存污染&#xff0c;我认为在自己学习大模型的过程用来省钱非常可以&#xff0c;再加上学习过…

网络安全全知识图谱:威胁、防护、管理与发展趋势详解

1 网络安全基础概念 1.1 什么是网络安全 网络安全是指通过技术、管理和法律等手段&#xff0c;保护计算机网络系统中的硬件、软件及其系统中的数据&#xff0c;不因偶然的或者恶意的原因而遭受到破坏、更改、泄露&#xff0c;确保系统连续可靠正常地运行&#xff0c;网络服务不…

远控安全进阶之战:TeamViewer/ToDesk/向日葵设备安全策略对比

【作者主页】Francek Chen 【文章摘要】在数字化时代&#xff0c;卓越的远程控制软件需兼顾功能与体验&#xff0c;包括流畅连接、高清画质、低门槛UI设计、毫秒级延迟及多功能性&#xff0c;同时要有独树一帜的远控安全技术&#xff0c;通过前瞻性安全策略阻挡网络风险&#x…

Steam发布游戏过程的若干问题

我没有想到在Steam发布游戏的过程会比做游戏的过程更困难&#xff0c;更恶心。 注册Steamworks 税务采访 税务采访部分填的地址要和后面它们要求你发证件照片里的地址一样。护照里因为没有地址不会通过&#xff0c;我用的驾照里面有地址。没有驾照可以用身份证。 应用准备界…

开搞:第四个微信小程序:图上县志

原因&#xff1a;我换了一个微信号来搞&#xff0c;因为用同一个用户&#xff0c;备案只能一个个的来。这样不行。所以我换了一个。原来注册过小程序。现在修改即可。注意做好计划后&#xff0c;速度备案和审核&#xff0c;不然你时间浪费不起。30元花起。 结构&#xff1a; -…

第三十七天打卡

知识点回顾&#xff1a; 过拟合的判断&#xff1a;测试集和训练集同步打印指标模型的保存和加载 仅保存权重保存权重和模型保存全部信息checkpoint&#xff0c;还包含训练状态 早停策略 作业&#xff1a;对信贷数据集训练后保存权重&#xff0c;加载权重后继续训练50轮&#x…

Java高频面试之并发编程-21

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本baby今天又来报道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面试官&#xff1a;详细说说AQS AQS&#xff08;AbstractQueuedSynchronizer&#xff09;是 Java 并发包&#xff08;java.util.concurre…

按键状态机

原工程地址&#xff1a;https://github.com/candylife9/state_machine_example 视频&#xff1a;C语言之状态机编程_02_状态机使用案例分析_哔哩哔哩_bilibili 我觉得讲的挺好的。 来自豆包封装的通用接口 头文件 /*** file key_state_machine.h* brief 通用按键状态机接口…

华为OD机试真题——新学校选址(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

欧拉操作系统下安装hadoop集群

背景&#xff1a;欧拉操作系统下安装CDH集群的时候&#xff0c;需要安装python2.7.5&#xff0c;但是本身欧拉系统对python2的支持可能没有那么好&#xff0c;所以考虑搭建原生的hadoop集群。 基础环境如下 组件名称组件版本欧拉VERSION“22.03 (LTS-SP4)”jdkopenjdk versio…

SQL语句的执行流程

文章目录 一、执行流程二、建立连接三、预处理器四、解析器4.1 词法分析4.2 语法分析4.3 语义分析 五、优化器六、执行器七、返回结果 一、执行流程 阶段主要功能关键组件1. 建立连接身份验证、权限检查连接器2. 预处理器缓存检查、SQL预处理查询缓存3. 解析器词法分析、语法分…

TiDB:从快速上手到核心原理与最佳实践

文章目录 引言第一部分&#xff1a;TiDB快速体验与实践指南1. TiDB概述2. TiDB部署方式2.1 本地测试环境部署2.2 生产环境部署2.3 Kubernetes部署2.4 云服务 3. TiDB基本操作3.1 连接TiDB3.2 数据库和表操作3.3 分区表3.4 事务操作 4. 数据迁移到TiDB4.1 从MySQL迁移4.2 使用Ti…

总结:进程和线程的联系和区别

前言:通过学习javaEE初阶中的多线程章节后加上我自己的理解,想来总结一下线程和进程的联系和区别. 一来是能更好地复习知识,二来是为了记录我的学习路程,相信未来的我回首不会忘记这段难忘的经历. 1.进程 先来谈谈进程:进程是操作系统中资源分配的基本单位. 1)进程的执行方…

边缘云的定义、实现与典型应用场景!与传统云计算的区别!

一、什么是边缘云&#xff1f;‌ 边缘云是一种‌分布式云计算架构‌&#xff0c;将计算、存储和网络资源部署在‌靠近数据源或终端用户的网络边缘侧‌&#xff08;如基站、本地数据中心或终端设备附近&#xff09;&#xff0c;而非传统的集中式云端数据中心。 ‌核心特征‌&…

海康威视摄像头C#开发指南:从SDK对接到安全增强与高并发优化

一、海康威视SDK核心对接流程​​ 1. ​​开发环境准备​​ ​​官方SDK获取​​&#xff1a;从海康开放平台下载最新版SDK&#xff08;如HCNetSDK.dll、PlayCtrl.dll&#xff09;。​​依赖项安装​​&#xff1a;确保C运行库&#xff08;如vcredist_x86.exe&#xff09;与S…

《软件工程》第 9 章 - 软件详细设计

目录 9.1 详细设计的任务与过程模型 9.2 用例设计 9.2.1 设计用例实现方案 9.2.2 构造设计类图 9.2.3 整合并优化用例实现方案 9.3 子系统设计 9.3.1 确立内部设计元素 9.3.2 导出设计类图 9.4 构件设计 9.5 类设计 9.5.1 精化类间关系 9.5.2 精化属性和操作 9.5.…

spring+tomcat 用户每次发请求,tomcat 站在线程的角度是如何处理用户请求的,spinrg的bean 是共享的吗

对于 springtomcat 用户每次发请求&#xff0c;tomcat 站在线程的角度是如何处理的 比如 bio nio apr 等情况 tomcat 配置文件中 maxThreads 的数量是相对于谁来说的&#xff1f; 以及 spring Controller 中的全局变量:各种bean 对于线程来说是共享的吗&#xff1f; 一、Tomca…

存储引擎系列--LSM不同Compaction策略性能分析对比

本文介绍一下参考论文里的Compaction性能分析部分,作者在RocksDB的基础上做了多种策略的改造,然后提出了benchmarking方法论,关注compaction性能的哪些维度,并对结果进行分析。 一、Standardization of Compaction Strategies 1.1 实验平台的选择 作者选择了RocksDB作为…

leetcode 3559. Number of Ways to Assign Edge Weights II

leetcode 3559. Number of Ways to Assign Edge Weights II 1. 解题思路2. 代码实现 题目链接&#xff1a;3559. Number of Ways to Assign Edge Weights II 1. 解题思路 这一题是题目3558. Number of Ways to Assign Edge Weights I的进阶版本。 对于题目3558来说&#xf…