Linux 线程互斥

目录

0.前言

1.相关概念

2.互斥量(mutex)

2.1 代码引入

2.2为什么需要互斥量

2.3互斥量的接口

2.3.1 初始化互斥量

2.3.2 销毁互斥量

2.3.3 互斥量加锁和解锁

2.4改写代码

 3.互斥量的封装

4.小结


(图像由AI生成) 

0.前言

在多线程编程中,线程之间的并发操作可能会导致共享资源的竞争问题,如数据不一致、状态紊乱等。为了保证程序的正确性和稳定性,必须引入线程同步机制,其中互斥量(mutex)是解决线程互斥的核心工具。本篇博客承接前文关于进程的讨论,深入介绍线程互斥的相关概念、实现方法以及代码实例,帮助理解如何在 Linux 环境下有效避免线程竞争问题。

1.相关概念

  • 临界资源:指多个线程需要共享访问的资源,例如全局变量、文件或数据库连接等。如果多个线程同时操作临界资源,可能会导致数据不一致或冲突。
  • 临界区:指访问临界资源的代码片段。为防止多个线程同时进入临界区,需要对其进行保护,确保同一时刻只有一个线程可以执行临界区代码。
  • 互斥:一种线程同步机制,用于确保多个线程对临界资源的访问是互斥的,即同一时间仅允许一个线程访问共享资源。互斥量(mutex)是实现互斥的常用工具。
  • 原子性:指某个操作不可被中断,要么完全执行完毕,要么完全不执行。在多线程环境下,原子性是实现线程安全的基本要求之一。

2.互斥量(mutex)

互斥量是一种线程同步机制,用于解决多线程并发访问共享资源时的冲突问题。在多线程编程中,互斥量通过对临界区的加锁和解锁,确保同一时刻只有一个线程可以访问共享资源,从而避免数据竞争。

2.1 代码引入

在大多数情况下,线程使用的数据是局部变量,变量的地址空间位于线程栈空间内,仅属于单个线程,其他线程无法访问。但在某些场景下,线程之间需要共享数据,这些变量称为共享变量,通过它们可以完成线程间的交互。

然而,当多个线程并发操作共享变量时,会导致数据不一致等问题。例如,一个典型的问题是多个线程争夺共享资源时的竞争。以下以“抢票”为例,展示未加锁的多线程争夺资源代码:

未加锁的多线程代码示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // usleep 函数

int ticket = 100; // 共享资源

void* sell_tickets(void* arg) {
    char* id = (char*)arg; // 将 void* 转为 char*
    while (1) {
        if (ticket > 0) { // 检查是否还有票
            usleep(1000); // 模拟售票的延迟
            printf("%s sells ticket: %d\n", id, ticket);
            ticket--; // 执行 -- 操作,存在数据竞争
        } else {
            break; // 没有票时退出
        }
    }
    return NULL;
}

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

    // 创建四个线程,并显式转换字符串为 void*
    pthread_create(&t1, NULL, sell_tickets, (void*)"thread 1");
    pthread_create(&t2, NULL, sell_tickets, (void*)"thread 2");
    pthread_create(&t3, NULL, sell_tickets, (void*)"thread 3");
    pthread_create(&t4, NULL, sell_tickets, (void*)"thread 4");

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

程序输出(部分):

thread 2 sells ticket: 100
thread 1 sells ticket: 100
thread 3 sells ticket: 100
thread 4 sells ticket: 100
...
thread 2 sells ticket: 3
thread 3 sells ticket: 3
thread 4 sells ticket: 1
thread 2 sells ticket: 0
thread 1 sells ticket: -1
thread 3 sells ticket: -2

2.2为什么需要互斥量

在多线程编程中,当多个线程并发访问共享资源时,如果没有同步机制进行保护,就会导致数据竞争和资源冲突等问题。以下是未加锁情况下上面的代码出现的问题:

  1. 票号重复销售:
    多个线程同时读取共享变量 ticket 的值,导致同一票号被多个线程同时销售。例如:

    thread 2 sells ticket: 100
    thread 1 sells ticket: 100
    thread 3 sells ticket: 100
    

    这是因为 ticket 的读取和更新是分步骤完成的,线程在切换时导致了数据的不一致。

  2. 超卖现象:
    由于多个线程同时修改 ticket 的值,可能导致最终结果错误,甚至出现负值。例如:

    thread 1 sells ticket: -1
    thread 3 sells ticket: -2
    

    这种现象表明线程在操作过程中缺乏有效的同步机制,无法确保共享变量的正确性。

  3. 数据竞争:
    ticket-- 是非原子操作,分为读取值、修改值和写回值三个步骤。在多线程环境下,这些步骤可能被其他线程的操作打断,导致多个线程同时更新变量的值,破坏数据一致性。

多线程编程中的共享资源竞争是导致数据不一致的主要原因。以抢票系统中的 --ticket 操作为例,尽管它看似简单,但实际上并非原子操作,而是由多条汇编指令组成的复杂过程。

--ticket 的汇编代码
通过 objdump 工具反汇编程序,我们可以看到 --ticket 的具体汇编指令:

152 40064b: 8b 05 e3 04 20 00    mov 0x2004e3(%rip),%eax   # 将共享变量加载到寄存器
153 400651: 83 e8 01             sub $0x1,%eax             # 更新寄存器中的值,执行 -1 操作
154 400654: 89 05 da 04 20 00    mov %eax,0x2004da(%rip)   # 将新值写回共享变量的内存地址

这三条指令的含义分别是:

  1. load: 将共享变量 ticket 的值从内存加载到寄存器。
  2. update: 在寄存器中执行 -1 操作,更新值。
  3. store: 将更新后的值写回共享变量的内存地址。

问题所在:
由于 --ticket 涉及三步操作,如果线程在任意步骤被中断,另一个线程可能会修改 ticket,导致数据竞争。例如:

  • 线程 A 从内存读取 ticket = 100,还未更新,线程 B 也读取了 ticket = 100
  • 两个线程都执行了 ticket-- 操作,结果是 ticket = 99,实际减少了一张票而非两张。

这种数据不一致问题会引发票号重复销售超卖现象,根本原因是 --ticket 不是原子操作。

如何解决这些问题?
为了解决共享资源的竞争问题,需要满足以下三点:

  1. 互斥行为: 当一个线程进入临界区执行代码时,其他线程必须被阻止进入临界区。
  2. 独占访问: 如果多个线程同时请求进入临界区,且临界区没有线程在执行,则仅允许一个线程进入。
  3. 非阻塞: 如果某线程不在临界区内执行,则不能阻止其他线程进入临界区。

这些条件的核心要求是一把锁,而 Linux 系统中提供的这把锁就是互斥量(mutex)

互斥量的作用:
互斥量通过加锁(pthread_mutex_lock)和解锁(pthread_mutex_unlock),实现对临界区的独占访问:

  • 加锁: 线程在访问共享资源前需要获得锁,如果其他线程已经持有锁,则当前线程会阻塞。
  • 解锁: 线程在完成共享资源操作后释放锁,其他阻塞线程才可以继续执行。

通过互斥量,--ticket 的多条汇编指令可以被视为一个原子操作,从而避免数据竞争,确保程序的正确性和线程安全。

2.3互斥量的接口

在多线程编程中,互斥量(mutex)提供了一种机制来确保共享资源的安全访问。以下介绍互斥量的核心操作接口。

2.3.1 初始化互斥量

互斥量在使用前需要进行初始化,主要有两种方法:

方法1:静态分配

通过宏 PTHREAD_MUTEX_INITIALIZER 初始化互斥量,适用于全局或静态互斥量:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

这种方式简单直接,适合在程序启动时确定的互斥量。

方法2:动态分配

通过函数 pthread_mutex_init 动态初始化互斥量,适用于动态创建的互斥量:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 动态初始化
  • 参数说明:
    • mutex:指向需要初始化的互斥量。
    • attr:互斥量属性,一般传 NULL 表示使用默认属性。

示例代码:

pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 动态初始化

2.3.2 销毁互斥量

互斥量使用完成后,需通过 pthread_mutex_destroy 释放资源:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 注意事项:
    1. 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要显式销毁。
    2. **不要销毁一个已经加锁的互斥量,**否则可能导致程序崩溃或行为异常。
    3. **确保销毁后的互斥量不再被使用,**避免线程尝试加锁销毁的互斥量。

示例代码:

pthread_mutex_destroy(&mutex);

2.3.3 互斥量加锁和解锁

加锁

使用 pthread_mutex_lock 对互斥量加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 行为:
    1. 如果互斥量处于未锁状态,调用线程会成功加锁并继续执行。
    2. 如果互斥量已被其他线程锁定,调用线程会阻塞,等待互斥量解锁。

解锁

使用 pthread_mutex_unlock 对互斥量解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 解锁后,其他等待的线程将有机会获得锁。

返回值:

  • 成功返回 0
  • 失败返回错误号(例如尝试解锁未加锁的互斥量)。

示例代码:

pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);

2.4改写代码

在 2.1 的示例代码中,由于 ticket-- 操作不是原子操作,导致出现数据竞争和不一致的问题。通过引入互斥量(mutex),可以确保对共享资源 ticket 的访问具有互斥性,从而解决上述问题。

以下是改写后的代码,使用互斥量实现线程安全:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // usleep 函数

int ticket = 100; // 共享资源
pthread_mutex_t mutex; // 定义互斥量

void* sell_tickets(void* arg) {
    char* id = (char*)arg; // 将 void* 转为 char*
    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); // 解锁,允许其他线程访问共享资源
    }
    return NULL;
}

int main(void) {
    pthread_mutex_init(&mutex, NULL); // 初始化互斥量

    pthread_t t1, t2, t3, t4;

    // 创建四个线程,并显式转换字符串为 void*
    pthread_create(&t1, NULL, sell_tickets, (void*)"thread 1");
    pthread_create(&t2, NULL, sell_tickets, (void*)"thread 2");
    pthread_create(&t3, NULL, sell_tickets, (void*)"thread 3");
    pthread_create(&t4, NULL, sell_tickets, (void*)"thread 4");

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    pthread_mutex_destroy(&mutex); // 销毁互斥量
    return 0;
}

 3.互斥量的封装

在实际开发中,直接操作互斥量可能会导致代码冗长且容易出错。通过对互斥量的封装,可以简化使用流程并提高代码的可维护性。以下通过 Lock.hpp 文件展示如何封装互斥量,并采用 RAII 风格实现自动化管理。

#pragma once
#include <pthread.h>

namespace LockModule {

// 对互斥量进行封装
class Mutex {
public:
    // 禁止拷贝构造和赋值
    Mutex(const Mutex &) = delete;
    const Mutex &operator=(const Mutex &) = delete;

    // 构造函数,初始化互斥量
    Mutex() {
        int n = pthread_mutex_init(&_mutex, nullptr);
        (void)n; // 忽略返回值,实际开发中可以添加错误检查
    }

    // 加锁
    void Lock() {
        int n = pthread_mutex_lock(&_mutex);
        (void)n;
    }

    // 解锁
    void Unlock() {
        int n = pthread_mutex_unlock(&_mutex);
        (void)n;
    }

    // 获取互斥量的原始指针
    pthread_mutex_t *GetMutexOriginal() {
        return &_mutex;
    }

    // 析构函数,销毁互斥量
    ~Mutex() {
        int n = pthread_mutex_destroy(&_mutex);
        (void)n;
    }

private:
    pthread_mutex_t _mutex; // 封装的互斥量
};

// RAII 风格的锁管理器
class LockGuard {
public:
    // 构造函数,自动加锁
    LockGuard(Mutex &mutex) : _mutex(mutex) {
        _mutex.Lock();
    }

    // 析构函数,自动解锁
    ~LockGuard() {
        _mutex.Unlock();
    }

private:
    Mutex &_mutex; // 引用封装的互斥量
};

}

封装的核心思想

  1. Mutex 类:

    • 封装了 pthread_mutex_t 的操作,包括初始化、加锁、解锁和销毁。
    • 禁止拷贝构造和赋值,避免多次操作同一个互斥量。
    • 提供获取原始互斥量指针的方法,以便在某些特殊场景中直接操作底层互斥量。
  2. LockGuard 类:

    • 采用 RAII(Resource Acquisition Is Initialization)风格,通过构造函数加锁,析构函数解锁,实现自动化管理。
    • 避免手动解锁可能导致的遗漏问题。

4.小结

线程间的共享资源竞争是多线程编程中的核心问题,互斥量(mutex)提供了一种高效的解决方案。通过本篇博客,我们从互斥量的基础概念入手,详细介绍了其初始化、加锁解锁操作,以及如何通过封装实现更安全和高效的资源管理。通过互斥量,我们可以确保临界区操作的线程安全性,避免数据竞争和资源冲突,为构建健壮的多线程应用奠定基础。

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

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

相关文章

前端实用知识-用express搭建本地服务器

目录 一、为什么会有这篇文章&#xff1f; 二、使用前的准备-如环境、工具 三、如何使用&#xff1f;-express常用知识点 四、代码演示-配合截图&#xff0c;简单易懂 一、为什么会有这篇文章&#xff1f; 在日常前端开发中&#xff0c;我们离不开数据&#xff0c;可能是用…

用nextjs开发时遇到的问题

这几天已经基本把node后端的接口全部写完了&#xff0c;在前端开发时考虑时博客视频类型&#xff0c;考虑了ssr&#xff0c;于是选用了nextJs&#xff0c;用的是nextUi,tailwincss,目前碰到两个比较难受的事情。 1.nextUI个别组件无法在服务器段渲染 目前简单的解决方法&…

【数据结构】二叉树(2)

目录 1. 二叉树的遍历 前序遍历 中序遍历 后序遍历 2. 计算二叉树中的节点个数 3. 计算二叉树中叶子节点个数 4. 计算二叉树的深度 5. 计算二叉树第k层节点个数 6. 二叉树基础练习 7. 二叉树的创建 8. 二叉树的销毁 9. 层序遍历 10. 判断二叉树是否为完全二叉树 1…

比特币与区块链原理解析:矿机挖矿与去中心化的未来

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

StarRocks-join优化

1、背景 有两个大表&#xff0c;都是6kw级别上下的&#xff0c;通过SR然后包装了一个接口对外提供查询&#xff0c;当前的问题是&#xff0c;这样大的join查询会导致BE直接宕机。并且这个sql很有代表性&#xff0c;我截图如下&#xff1a; 这个表是个单分区&#xff0c;所以直接…

Qt中2D绘制系统

目录 一、Qt绘制系统 1.1Qt绘制基本概念 1.2 绘制代码举例 1.3画家 1.3.1 QPainter的工作原理&#xff1a; 1.3.2 自定义绘制饼状图&#xff1a; 1.4画笔和画刷 1.4.1画笔 1.4.2 画刷填充样式 1.5 反走样和渐变 1.6绘制设备 1.7坐标变换 1.8QPainterPath 1.9绘制文…

基于.NET调用WebService服务

基于.NET调用WebService服务 上一篇文章用java的Spring Boot框架搭建了一个WebService服务端&#xff0c;这篇文章通过.NET进行调用&#xff0c;下文基于Visual Studio 2022 引入WebService服务 项目右键 -> 添加 -> 服务引用 选择WCF Web Service&#xff0c;点击下一…

IIC 随机写+多次写 可以控制写几次

verilog module icc_tx#(parameter SIZE 2 , //用来控制写多少次 比如地址是0000 一个地址只能存放8bit数据 超出指针就会到下一个地址0001parameter CLK_DIV 50_000_000 ,parameter SPEED 100_000 ,parameter LED 50 )( input wire c…

微信小程序+Vant-自定义选择器组件(多选

实现效果 无筛选&#xff0c;如有需要可参照单选组件中的方法.json文件配置"component": true,columns需要处理成含dictLabel和dictValue字段&#xff0c;我是这样处理的&#xff1a; let list arr.map(r > {return {...r,dictValue: r.xxxId,dictLabel: r.xxx…

基于边缘智能网关的机房安全监测应用

随着我国工业互联网的扎实推进&#xff0c;越来越多地区积极建设信息基础设施&#xff0c;以充沛算力支撑产业物联网的可持续发展&#xff0c;数据机房就是其中的典型代表。而且随着机房规模的扩大&#xff0c;对于机房的安全管理难题挑战也日益增加。 面向数据机房安全监测与管…

HarmonyOS 应用跨团队 Debug 协作

文章目录 前言案例背景与问题分析问题背景问题分析工具 方法与代码实现前端模块的优化&#xff1a;日志记录与网络监听日志记录代码示例代码解析实现逻辑实际应用场景 网络状态监听代码示例代码解析实现逻辑实际应用场景 后端模块的优化&#xff1a;接口性能与容错机制接口性能…

《UnityShader 入门精要》更复杂的光照

代码&示例图见&#xff1a;zaizai77/Shader-Learn: 实现一些书里讲到的shader 到了这里就开启了书里的中级篇&#xff0c;之后会讲解 Unity 中的渲染路径&#xff0c;如何计算光照衰减和阴影&#xff0c;如何使用高级纹理和动画等一系列进阶内容 Unity 中的渲染路径 在U…

Ubuntu20.04安装kalibr

文章目录 环境配置安装wxPython下载编译测试报错1问题描述问题分析问题解决 参考 环境配置 Ubuntu20.04&#xff0c;python3.8.10&#xff0c;boost自带的1.71 sudo apt update sudo apt-get install python3-setuptools python3-rosinstall ipython3 libeigen3-dev libboost…

P1198 [JSOI2008] 最大数

P1198 [JSOI2008] 最大数https://www.luogu.com.cn/problem/P1198 牵制芝士&#xff1a;单调队列 思路&#xff1a; 我们的任务是找出一个区间最大值的 因为插入的数与上一次的答案有关 所以它是强制在线的&#xff08;真无语了&#xff09; 我们可以在每次插入时整一个叫…

宠物电商对接美团闪购:实现快速配送与用户增值

随着宠物行业的快速发展&#xff0c;宠物电商市场也在不断扩张。消费者的需求不再局限于传统的线上购物模式&#xff0c;越来越多的人开始追求更快捷的配送服务和更优质的购物体验。为了适应这一趋势&#xff0c;许多宠物电商平台开始寻求与本地配送平台合作&#xff0c;以提供…

阿里云oss转发上线-实现不出网钓鱼

本地实现阿里云oss转发上线&#xff0c;全部代码在文末&#xff0c;代码存在冗余 实战环境 被钓鱼机器不出网只可访问内部网络包含集团oss 实战思路 若将我们的shellcode文件上传到集团oss上仍无法上线&#xff0c;那么就利用oss做中转使用本地转发进行上线&#xff0c;先发送…

新型大语言模型的预训练与后训练范式,阿里Qwen

前言&#xff1a;大型语言模型&#xff08;LLMs&#xff09;的发展历程可以说是非常长&#xff0c;从早期的GPT模型一路走到了今天这些复杂的、公开权重的大型语言模型。最初&#xff0c;LLM的训练过程只关注预训练&#xff0c;但后来逐步扩展到了包括预训练和后训练在内的完整…

C#结构体排序(数组)

结构体排序&#xff08;数组&#xff09; 1 示例1.1 以PointF为例展示效果1.2 运行结果展示 2实际运用2.1 创建结构体2.2 调用示例2.3 运行结果展示 1 示例 1.1 以PointF为例展示效果 private void button1_Click(object sender, EventArgs e) {Random random new Random();…

前端高频面试题-并发请求

面试题中&#xff0c;有一道题经常会出现&#xff0c;咱们下面讲一下思路以及写法写一个方法&#xff0c;传入一个请求地址数组&#xff0c;以及一个并发数量&#xff0c;根据并发数量&#xff0c;一起发送请求。如果一个发送完&#xff0c;那么从数组中拿出来一个接着发送&…

RabbitMQ7:消息转换器

欢迎来到“雪碧聊技术”CSDN博客&#xff01; 在这里&#xff0c;您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者&#xff0c;还是具有一定经验的开发者&#xff0c;相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导&#xff0c;我将…