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

C++ 多线程进阶:互斥锁解决竞态条件实战

多线程环境下共享资源访问易引发竞态条件,导致数据错乱。通过 C++ 买票案例演示 check-then-act race 原理,展示互斥锁如何解决此问题。重点讲解临界区概念、锁的粒度控制及解锁时机,强调避免死锁和锁内休眠的陷阱。互斥锁保障互斥但不保证公平,理解这些机制是编写安全并发程序的基础。

修罗发布于 2026/3/15更新于 2026/7/2733 浏览
C++ 多线程进阶:互斥锁解决竞态条件实战

引言

在之前的多线程学习中,我们常遇到乱码或抢占输出的问题。这背后往往隐藏着更深层的并发隐患。本章我们通过一个经典的买票场景,深入剖析线程间的竞争关系,并学习如何用互斥锁来保障数据安全。

问题的根源:竞态条件

假设我们有 100 张电影票,5 个线程同时抢票。如果直接操作共享变量 ticket,会发生什么?

#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.h>

int ticket = 100;

void routine(std::string name) {
    while (true) {
        if (ticket > 0) {
            usleep(1000); // 模拟抢票耗时
            ticket--;
            printf("%s shell ticket, now tickets number:%d\n", name.c_str(), ticket);
        } else {
            std::cout << ticket << std::endl;
            break;
        }
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; i++) {
        std::string name = "thread-";
        name += std::to_string(i);
        threads.emplace_back(routine, name);
    }
    for (auto& thread : threads) {
        thread.join();
    }
    return 0;
}

这里的 ticket 是公共资源。理论上票数减到 0 就该停,但实际运行结果可能变成 -4。这是因为多线程环境下的竞态条件(Race Condition)。

为什么会发生这种情况

单线程不会有问题,但多线程存在资源竞争。每个线程读取 ticket、判断、休眠、再写入的过程不是原子的。

当票数为 1 时:

  1. 线程 A 读取 ticket 为 1,准备执行减法。
  2. 线程 A 休眠(usleep),此时 CPU 切换给线程 B。
  3. 线程 B 也读取 ticket 为 1,同样准备执行减法。
  4. 线程 A 醒来,将 ticket 减为 0 写回内存。
  5. 线程 B 醒来,基于之前读取的值再次减 1,写回 -1。

这就是经典的 Check-Then-Act Race。底层汇编逻辑大致如下:

; if (ticket > 0)
LOAD R1, [ticket]   ; R1 = ticket
CMP R1, 0           ; 比较 R1 和 0
JLE END_IF          ; 如果 <= 0,跳走

; usleep(1000)
CALL usleep

; ticket--
LOAD R2, [ticket]   ; R2 = 当前 ticket (注意这里重新读了一次)
SUB R2, 1           ; R2 = R2 - 1
STORE [ticket], R2  ; 写回 ticket
END_IF:

关键点在于:判断时用的是旧值,真正减法时又重新读了一次内存。如果中间被其他线程修改了数据,当前线程就会基于错误的情报进行操作。

引入互斥锁

为了避免这种乌龙事件,我们需要引入锁的概念。互斥锁(Mutex)能保证同一时间只有一个线程持有锁,从而独占临界区。

#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void routine(std::string name) {
    while (true) {
        pthread_mutex_lock(&lock); // 加锁
        if (ticket > 0) {
            usleep(1000);
            ticket--;
            printf("%s shell ticket, now tickets number:%d\n", name.c_str(), ticket);
            pthread_mutex_unlock(&lock); // 解锁
        } else {
            pthread_mutex_unlock(&lock); // 解锁
            break;
        }
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; i++) {
        std::string name = "thread-";
        name += std::to_string(i);
        threads.emplace_back(routine, name);
    }
    for (auto& thread : threads) {
        thread.join();
    }
    return 0;
}

加上锁之后,只有拿到锁的线程才能进入 if 块操作 ticket,其他线程会被阻塞等待。这样就能保证票数不会变成负数。

临界区与锁的范围

为什么要加锁?核心是为了避免竞争。访问共享资源(如全局变量、文件等)的那部分代码称为临界区(Critical Section)。

核心规则:同一时刻,只允许一个线程进入临界区。 保护方式:进入前加锁,离开后解锁。

解锁时机与死锁风险

注意上面的代码,无论 if 还是 else 分支,都在最后释放了锁。如果写成下面这样:

pthread_mutex_lock(&lock);
if (ticket > 0) {
    // ... 操作 ...
    // pthread_mutex_unlock(&lock); // 忘记解锁了
} else {
    // pthread_mutex_unlock(&lock); // 忘记解锁了
    break;
}
pthread_mutex_unlock(&lock); // 这里永远执行不到

一旦线程在持有锁的情况下直接退出(如 break、return 或异常),而未释放锁,其他需要该锁的线程将永远等待。这就是典型的死锁诱因之一。

死锁产生的四个必要条件(Coffman 条件)包括:互斥、占有并等待、不可剥夺、循环等待。虽然这里只是简单的未解锁,但理解这些有助于排查复杂死锁。

锁的粒度:别抱着锁睡觉

还有一个细节容易被忽视:usleep(1000) 应该放在锁内还是锁外?

如果在锁内休眠,意味着持有锁的线程在睡觉,其他想抢票的线程只能干等着。这是非常不合理的资源浪费。

但在本例中,为了演示互斥效果,我们将 usleep 放在锁内模拟'处理过程'。在实际工程中,耗时操作应尽量移出临界区,只保护对共享数据的读写操作。

修正后的推荐写法:

void routine(std::string name) {
    while (true) {
        pthread_mutex_lock(&lock);
        if (ticket > 0) {
            ticket--;
            printf("%s shell ticket, now tickets number:%d\n", name.c_str(), ticket);
            pthread_mutex_unlock(&lock);
        } else {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
}

互斥锁不保证公平

你可能会发现,某个线程似乎一直在卖票。这说明这段时间里它反复拿到了 CPU,并且每次也都先抢到了那把锁。

互斥锁只保证了互斥性(Mutual Exclusion),即同一时间只有一个线程能访问资源,但它并不保证公平性(Fairness)。谁抢到 CPU 且先调用 pthread_mutex_lock,谁就有机会先拿到锁。如果需要公平调度,通常需要配合条件变量或其他同步机制。

总结

多线程环境下,共享资源访问易引发竞态条件,导致数据错乱(如负数车票)。互斥锁是解决此类问题的基础工具,通过锁定临界区确保原子性操作。

掌握互斥锁的关键点在于:

  1. 临界区范围:仅包裹访问共享资源的代码,避免包含耗时操作。
  2. 解锁完整性:所有分支路径都必须记得解锁,防止死锁。
  3. 公平性认知:锁只保安全,不保顺序,高并发下需关注调度策略。

打好这些基本功,才能在并发编程中写出既高效又安全的代码。

目录

  1. 引言
  2. 问题的根源:竞态条件
  3. 为什么会发生这种情况
  4. 引入互斥锁
  5. 临界区与锁的范围
  6. 解锁时机与死锁风险
  7. 锁的粒度:别抱着锁睡觉
  8. 互斥锁不保证公平
  9. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 机器人具身智能:核心定义、指标与标准体系
  • 基于 Termux 的 Android 平台 OpenClaw 部署:移动端 AI 助理实现
  • 大模型学习路线:从零基础到精通的进阶指南
  • StreamVLN 具身导航复现与推理指南
  • 基于 C++ 的第三方 SDK 封装实践:ASR 与短信服务
  • 基于 PX4 与 Mid360 激光雷达的无人机 FAST-LIO 定点悬停
  • MySQL 迁移金仓数据库:高兼容与自动化低成本落地方案
  • 网易 LobsterAI 0.2.2 实战:部署企业微信与 QQ AI Agent
  • HTML 核心语法与常用标签入门指南
  • OpenClaw 系统架构分析
  • ComfyUI_smZNodes 安装指南:实现跨平台 AI 绘画效果一致
  • Copilot Cowork 核心逻辑与 Kotlin AI Agent 实现
  • AI 写作软件推荐:多场景实用工具整理
  • 国产 AI 智能体平台对比:腾讯、字节、阿里、百度等主流方案汇总
  • Python 文本转语音:Edge TTS 库使用指南
  • 开源开发工具精选与 AI 大模型学习路径解析
  • 网络安全入门:成为白帽黑客的学习路线指南
  • OpenClaw 多飞书机器人与多 Agent 协作落地记录
  • 前端精确数字运算:使用 BigNumber.js 解决 JavaScript 精度问题
  • 企业微信外部群 Webhook 配置与消息推送指南

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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