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

news2025/6/1 2:16:34

个人主页: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;  // 定义线程 ID
        pthread_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.coloradmin.cn/o/2392276.html

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

相关文章

JWT安全:假密钥.【签名随便写实现越权绕过.】

JWT安全&#xff1a;假密钥【签名随便写实现越权绕过.】 JSON Web 令牌 (JWT)是一种在系统之间发送加密签名 JSON 数据的标准化格式。理论上&#xff0c;它们可以包含任何类型的数据&#xff0c;但最常用于在身份验证、会话处理和访问控制机制中发送有关用户的信息(“声明”)。…

Python爬虫实战:抓取百度15天天气预报数据

&#x1f310; 编程基础第一期《9-30》–使用python中的第三方模块requests&#xff0c;和三个内置模块(re、json、pprint)&#xff0c;实现百度地图的近15天天气信息抓取 记得安装 pip install requests&#x1f4d1; 项目介绍 网络爬虫是Python最受欢迎的应用场景之一&…

RV1126 + FFPEG多路码流项目

代码主体思路&#xff1a; 一.VI,VENC,RGA模块初始化 1.先创建一个自定义公共结构体&#xff0c;用于方便管理各个模块 rkmedia_config_public.h //文件名字#ifndef _RV1126_PUBLIC_H #define _RV1126_PUBLIC_H#include <assert.h> #include <fcntl.h> #include …

NodeJS 基于 Koa, 开发一个读取文件,并返回给客户端文件下载,以及读取文件形成列表和文件删除的代码演示

前言 在上一篇文章 《Nodejs 实现 Mysql 数据库的全量备份的代码演示》 中&#xff0c;我们演示了如何将用户的 Mysql 数据库进行备份的代码。但是&#xff0c;这个备份&#xff0c;只是备份在了服务器上了。 而我们用户的真实需求&#xff0c;是需要将备份文件下载到本地进行…

为什么在我的Flask里面有两个路由,但是在网页里有一个却不能正确访问到智能体

1. /zhoushibo 能访问&#xff0c;/chat 直接浏览器访问报 Method Not Allowed 原因&#xff1a; /zhoushibo 路由是你用 app.route(/zhoushibo) 定义的&#xff0c;返回的是一个HTML网页&#xff0c;浏览器访问没问题。 /chat 路由你用的是 app.route(/chat, methods[POST])…

哈工大计算机系统2024大作业——Hello的程序人生

计算机系统 大作业 题 目 程序人生-Hello’s P2P 专 业 人工智能 学   号 2022112040 班 级 2203601 学 生 郄东昕 指 导 教 师 吴锐 计算机科学与技术学院…

2025年软件测试面试八股文(含答案+文档)

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 Part1 1、你的测试职业发展是什么&#xff1f; 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师…

Flutter3.22适配运行鸿蒙系统问题记录

Flutter3.22适配运行鸿蒙系统问题记录 一&#xff1a;适配条件适配过程问题记录&#xff08;1&#xff09;环境配置问题&#xff08;2&#xff09;Concurrent modification during iteration: Instance(length:2) of_GrowableList 报错&#xff08;3&#xff09;三方插件寻找替…

秋招Day10 - JVM - 内存管理

JVM组织架构主要有三个部分&#xff1a;类加载器、运行时数据区和字节码执行引擎 类加载器&#xff1a;负责从文件系统、网络或其他来源加载class文件&#xff0c;将class文件中的二进制数据加载到内存中运行时数据区&#xff1a;运行时的数据存放的区域&#xff0c;分为方法区…

Spring Boot 3.5.0中文文档上线

Spring Boot 3.5.0 中文文档翻译完成&#xff0c;需要的可收藏 传送门&#xff1a;Spring Boot 3.5.0 中文文档

Redisson学习专栏(一):快速入门及核心API实践

文章目录 前言一、Redisson简介1.1 什么是Redisson&#xff1f;1.2 解决了什么问题&#xff1f; 二、快速入门2.1 环境准备 2.2 基础配置三、核心API解析3.1 分布式锁&#xff08;RLock&#xff09;3.2 分布式集合3.2.1 RMap&#xff08;分布式Map&#xff09;3.2.2 RList&…

Pandas学习入门一

1.什么是Pandas? Pandas是一个强大的分析结构化数据的工具集&#xff0c;基于NumPy构建&#xff0c;提供了高级数据结构和数据操作工具&#xff0c;它是使Python成为强大而高效的数据分析环境的重要因素之一。 一个强大的分析和操作大型结构化数据集所需的工具集基础是NumPy…

基于Piecewise Jerk Speed Optimizer的速度规划算法(附ROS C++/Python仿真)

目录 1 时空解耦运动规划2 PJSO速度规划原理2.1 优化变量2.2 代价函数2.3 约束条件2.4 二次规划形式 3 算法仿真3.1 ROS C仿真3.2 Python仿真 1 时空解耦运动规划 在自主移动系统的运动规划体系中&#xff0c;时空解耦的递进式架构因其高效性与工程可实现性被广泛采用。这一架…

游戏引擎学习第312天:跨实体手动排序

运行游戏并评估当前状况 目前排序功能基本已经正常&#xff0c;能够实现特定的排序要求&#xff0c;针对单一区域、单个房间的场景&#xff0c;效果基本符合预期。 不过还有一些细节需要调试。现在有些对象的缩放比例不对&#xff0c;导致它们看起来有些怪异&#xff0c;需要…

智警杯备赛--数据库管理与优化及数据库对象创建与管理

sql操作 插入数据 如果要操作数据表中的数据&#xff0c;首先应该确保表中存在数据。没有插入数据之前的表只是一张空表&#xff0c;需要使用insert语句向表中插入数据。插入数据有4种不同的方式&#xff1a;为所有字段插入数据、为指定字段插入数据、同时插入多条数据以及插…

MySQL 在 CentOS 7 环境下的安装教程

&#x1f31f; 各位看官好&#xff0c;我是maomi_9526&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 今天来学习Mysql的相关知识。 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&#xff0c;分享给更…

K8S集群主机网络端口不通问题排查

一、环境&#xff1a; k8s: v1.23.6 docker: 20.10.14 问题和故障现象&#xff1a;devops主机集群主机节点到端口8082不通&#xff08;网络策略已经申请&#xff0c;并且网络策略已经实施完毕&#xff09;&#xff0c;而且网络实施人员再次确认&#xff0c;网络策…

python打卡day39

知识点回顾 图像数据的格式&#xff1a;灰度和彩色数据模型的定义显存占用的4种地方 模型参数梯度参数优化器参数数据批量所占显存神经元输出中间状态 batchisize和训练的关系 课程代码&#xff1a; # 先继续之前的代码 import torch import torch.nn as nn import torch.opti…

3.8.5 利用RDD统计网站每月访问量

本项目旨在利用Spark RDD统计网站每月访问量。首先&#xff0c;创建名为“SparkRDDWebsiteTraffic”的Maven项目&#xff0c;并添加Spark和Scala的依赖。接着&#xff0c;编写Scala代码&#xff0c;通过SparkContext读取存储在HDFS上的原始数据文件&#xff0c;使用map和reduce…

尚硅谷redis7 49-51 redis管道之理论简介

前提redis事务和redis管道有点像&#xff0c;但本质上截然不同 49 redis管道之理论简介 面试题 如何优化频繁命令往返造成的性能瓶颈&#xff1f; redis每秒可以承受8万的写操作和接近10万次以上的读操作。每条命令都发送、处理、返回&#xff0c;能不能批处理一次性搞定呢…