跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

多线程数据竞争解析:互斥锁与原子操作原理

综述由AI生成多线程并发访问共享变量会导致数据不一致问题,如抢票程序出现票数负数。核心在于临界资源与临界区的概念,需通过互斥机制将并发执行转为串行。互斥锁(Mutex)保护临界区,但锁本身也需原子性保证,底层依赖硬件指令(如 swap/exchange)。C++11 提供了 std::mutex 及 lock_guard 简化管理,RAII 模式可自动处理加解锁,避免死锁与资源泄露。

赛博行者发布于 2026/2/7更新于 2026/6/228 浏览
多线程数据竞争解析:互斥锁与原子操作原理

线程互斥

大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

int tickets = 1000;

void *routel(void* args) {
    const char *name = (const char*)args;
    while (true) {
        if(tickets > 0) {
            usleep(10000);
            printf("%s 抢占票号:%d\n", name, tickets--);
        } else {
            break;
        }
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, routel, (void*)"thread-1");
    pthread_create(&t2, nullptr, routel, (void*)"thread-2");
    pthread_create(&t3, nullptr, routel, (void*)"thread-3");
    pthread_create(&t4, nullptr, routel, (void*)"thread-4");
    
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
     ;
}
return
0

上面这段程序中,tickets 就是所谓的共享资源,而这个共享资源是没有被保护的。我们创建了一批线程,让这几个线程都对这个共享资源进行 -- 操作,当达到 0 的时候自动退出被回收。

可是,我们观察到两个现象:打印消息出现错乱是为什么?为什么我们的票被抢到了负数呢?

多执行流向同一个显示器进行写入

多执行流向同一个显示器进行写入时,而显示器本身就是一个共享资源,才导致了打印消息可能错乱 --> 多线程面临的问题,因为多线程大部分资源都是共享的(幸运的话,我们甚至可以看见两个线程打印出现在同一行)。

为解决票被抢到负数的问题,需要深刻理解下面的概念:

进程线程间的互斥相关背景

临界资源:多线程执行流共享的资源就叫做临界资源。

临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

  • 互斥本质:是把多线程访问资源由并发执行转变成串行执行。
  • 正面意义:保护共享资源。
  • 负面意义:降低程序运行效率。

在一个线程 malloc 一个资源的全局变量,这个资源可以被其他线程看到,但能看到不代表能访问。临界区访问临界资源造成异常并不是因为共享了资源,而是因为共享了资源的同时,多线程还进行了并发访问导致的。共享资源不一定会导致数据不一致问题,访问共享资源才可能会导致数据不一致问题。

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

  • 互斥的出现才有了原子性:取钱的动作就是原子的:要么没人访问,要么上一个取完了,站在外人角度,你取钱动作就是原子的,对我就是有意义,在你访问期间,我对你产生不了影响。

原子性示意图

算逻运算

算逻运算图

数据在内存当中,计算在 CPU 中,就注定了一次完整的运算。

为什么会出现票被抢到负数的原因呢?

当前正在执行线程 A 的代码,此时经过了逻辑运算进入了判断体内,但因为 usleep 或时间片到了被切走了,要保存硬件上下文,带走了数据 1000;调度线程 B,做了 tickets--,把票数干到了 0,退出。此时线程 A 被唤回来了,还认为票数为 1000,执行 printf,tickets--,重新获取内存的值 0,进行算术运算,改为了 -1,修改 tickets 本身的值,写回到物理内存。

互斥锁

我们出现问题的原因是对 tickets 进行了 -- 操作,那么针对全局变量,进行 -- 或者 ++ 操作,又是否是安全的呢?即是否保证了所谓的原子性呢?

我们清楚:-- 或 ++ 操作被汇编后会形成多条语句,说明并不是原子的啊!

; 取出 ticket--部分的汇编代码 objdump -d a.out > test.objdump
152 40064b:  8b 05 e3 04 20 00  mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651:  83 e8 01          sub $0x1,%eax
154 400654:  89 05 da 04 20 00  mov %eax,0x2004da(%rip) # 600b34 <ticket>

多线程并发访问全局变量,因为汇编问题,导致不安全的,导致数据不一致问题。

操作并不是原子操作,而是对应三条汇编指令:

  • load:将共享变量 ticket 从内存加载到寄存器中
  • update:更新寄存器里面的值,执行 -1 操作
  • store:将新值,从寄存器写回共享变量 ticket 的内存地址

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

本质就是要对:对代码进行保护,形成临界区!--> pthread 库提出来一个互斥锁的概念 (可编程的)。

这里讲个小故事:

你是某位大学生,每天都抱着笔记本跑图书馆进行学习,而图书馆有一个条款,自习室只允许一个人进去且只有一把钥匙,当一个人进去的时候,其他人只能等待这个人出来归还钥匙或者不去争钥匙。为了争夺此钥匙,你很早就来图书馆拿到了钥匙,并打开自习室开始了学习,在你进入自习室期间,没有人会来打扰你。而当你学完了,就可以把钥匙进行归还。

可是,你肚子突然痛了啊!而你学习才学一半啊!你此时在想要不要把钥匙进行归还呢?经过深思熟虑,你把钥匙一同带进了厕所。此时,就没有人能打开这间自习室了啊!等你上完厕所依然可以进自习室进行学习。

锁操作

静态分配: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态分配: int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参数:mutex:要初始化的互斥量 attr:NULL 如果是栈上开辟的,需要对这个局部变量进行初始化

int pthread_mutex_destroy(pthread_mutex_t *mutex);使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁 (即如果是全局的,不需要销毁互斥量)。不要销毁一个已经加锁的互斥量,已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

int pthread_mutex_lock(pthread_mutex_t *mutex);互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么 pthread_lock 调用会陷入阻塞 (执行流被挂起),等待互斥量解锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);返回值:成功返回 0,失败返回错误号

int tickets = 1000;
pthread_mutex_t lock;

void *routel(void* args) {
    const char *name = (const char*)args;
    while (true) {
        pthread_mutex_lock(&lock);
        if(tickets > 0) {
            usleep(10000);
            printf("%s 抢占票号:%d\n", name, tickets--);
            pthread_mutex_unlock(&lock);
        } else {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}

对某个线程来说,要么你这个线程还没访问,要么访问完了,对他来讲才有意义。 可不可以理解成在加锁和解锁这部分过程是原子的?不会被别人打扰,不就是一种逻辑上的原子概念吗?原子是加锁后诞生的结果。

锁操作示意图

观点:锁的使用的最佳实践:加锁和解锁时,囊括的临界区尽量是最小集。

原子性

既然我们说加锁能对全局 tickets 进行保护,可是我们的 gmutex 自己不就是全局变量吗?怎么保证自己是安全的?

锁本身就是临界资源 --> 意味着要保护好自己才能保护别人 --> 如何保证加锁和解锁是安全的? --> 锁肯定要保证是原子的!

互斥锁是如何保证原子性的?

有多条代码要执行,可能随时会被时钟中断,时间片到了被切换; 如果在执行自己代码期间,不会被任何人打扰,执行 n 条指令一定是原子的。 问题是,如何做到不被打扰的? 背景:系统在自身的时间片调度范围内,要么主动让出自己的 CPU 资源,调用 read 系统调用,发现资源并不就绪,把自己的资源出让了;进程正常跑,但因为时钟中断可能由于时间片到了被切换了。

  • 硬件实现方案:关闭中断!
  • 软件实现方案:一条汇编语句 (保证原子)

内存搬到 CPU 资源时,是要拷贝的 (意味着可能存在多个线程对未改变的值进行拷贝)。

概念预备:swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

原子性底层实现

本质是谁先把锁里面的 '1' 交换到自己的硬件上下文里!!!

原生 C++11 mutex 抢票 Demo

#include <iostream>
#include <thread>
#include <mutex>
#include <unistd.h>

std::mutex mtx; // 定义一个互斥锁
int tickets = 10; // 初始票数

void routel(const std::string& name) {
    while (true) {
        // 使用 lock_guard 来锁定 mutex,确保线程安全
        std::lock_guard<std::mutex> lock(mtx);
        if (tickets > 0) {
            usleep(10000); // 模拟抢票的延时
            printf("%s 抢占票号:%d\n", name.c_str(), tickets--);
        } else {
            break;
        }
    }
}

int main() {
    // 创建一批线程,每个线程都去执行抢票逻辑
    std::thread t1(routel, "thread-1");
    std::thread t2(routel, "thread-2");
    std::thread t3(routel, "thread-3");
    std::thread t4(routel, "thread-4");
    
    // 等待所有线程完成
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return 0;
}

总结

本文深入探讨了 Linux 线程互斥机制,从多线程共享资源引发的问题出发,分析了临界资源与临界区的概念。通过'抢票'案例展示了多线程并发访问共享变量导致的数据不一致问题,并解析了汇编层面的非原子操作原理。文章详细介绍了互斥锁的使用方法,包括初始化、加锁、解锁等操作,并阐述了锁的原子性保证机制(硬件层面通过交换指令实现)。最后提供了 C++11 mutex 实现案例和基于 RAII 思想的互斥量封装方案,强调锁的最佳实践应保持临界区最小化。全文系统性地解决了多线程编程中的共享资源保护问题,兼顾理论分析与实践指导。

互斥量的封装

class Mutex {
public:
    Mutex() {
        // 初始化锁
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock() {
        // 加锁
        pthread_mutex_lock(&_lock);
    }
    void Unlock() {
        // 解锁
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex() {
        // 毁坏锁
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard {
public:
    LockGuard(Mutex* mutex):_mutexp(mutex) {
        _mutexp->Lock();
    }
    ~LockGuard() {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};

上面这段代码采用了 RAII 风格的加锁机制。

RAII 的核心思想是将资源(比如内存、文件句柄、锁等)的管理和生命周期绑定到对象的生命周期上。具体来说,当一个对象被创建时,它获取相关资源(比如加锁),而当对象超出作用域时(即销毁),它自动释放资源(比如解锁)。这种方式确保了资源总能被正确地释放,避免了资源泄露或死锁的风险。

在这段代码中,LockGuard 类是 RAII 的典型实现,它自动地在对象创建时加锁,在对象销毁时解锁,避免了手动管理锁的繁琐和可能的错误。

Mutex lock;
struct data {
    std::string name;
    pthread_mutex_t *lock;
};

void *routel(void *args) {
    data *d = static_cast<data *>(args);
    while (true) {
        {
            LockGuard lockguard(&lock);
            if (tickets > 0) {
                usleep(10000);
                printf("%s 抢占票号:%d\n", (d->name).c_str(), tickets--);
            } else {
                break;
            }
        }
    }
    return nullptr;
}

封装示例

目录

  1. 线程互斥
  2. 进程线程间的互斥相关背景
  3. 算逻运算
  4. 互斥锁
  5. 锁操作
  6. 原子性
  7. 原生 C++11 mutex 抢票 Demo
  8. 总结
  9. 互斥量的封装
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 鸿蒙金融理财全栈项目——生态合作、用户运营、数据变现
  • OpenAI 发布 GPT-4o 多模态模型及接入方式详解
  • Wildcard AI 服务评测:低价背后的模型真相
  • Stack-Chan 机器人开发指南:从入门到进阶
  • 数据结构基础:顺序表的定义与实现
  • VR-Reversal 插件实现 3D 视频转 2D 格式教程
  • C++ 多线程进阶:互斥锁解决竞态条件
  • PyTorch scatter() 与 scatter_() 用法详解
  • 安路 FPGA 下载器驱动安装与测试教程
  • 2026 年值得关注的十大 JavaScript 框架
  • Python 三元运算符详解
  • MySQL 表约束核心指南:从基础到外键实战
  • STL map 与 multimap 核心特性及接口详解
  • Visual C++ 运行库安装方案与常见 DLL 缺失问题修复
  • llama.cpp 安装和配置指南
  • 滑动窗口算法:高效处理子数组和子串问题
  • Linux 开发工具:GDB 调试器使用指南
  • 2023 年主流 Python 解释器深度解析与选型指南
  • GitHub Copilot 配置避坑与最佳实践指南
  • 春晚机器人刷屏,A 股板块为何高开低走?

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online