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

C++ 多线程同步:互斥锁 mutex 实战指南

C++ 多线程编程中,多个线程同时访问共享资源会引发数据竞争,导致结果不可预测。本文通过实例演示如何利用 std::mutex 和 std::lock_guard 解决资源竞争问题。重点讲解了互斥锁的基本接口、RAII 机制下的自动锁管理、死锁产生的四个必要条件及其规避策略,如固定锁获取顺序和使用 std::lock。最后通过多线程售票系统的实战案例,展示了互斥锁在实际场景中的应用,确保并发环境下的数据一致性与安全性。

PhpPioneer发布于 2026/3/24更新于 2026/10/869 浏览
C++ 多线程同步:互斥锁 mutex 实战指南

C++ 多线程同步:互斥锁 mutex 实战指南

在多线程编程中,当多个线程同时访问共享资源时,资源竞争几乎是不可避免的。比如两个线程同时对同一个变量进行读写操作,最终结果往往与预期不符。这就是典型的线程安全问题,而解决它的核心手段就是线程同步。

⚠️ 注意:如果线程不同步,数据竞争会导致程序运行结果不可预测,严重时甚至引发崩溃。

多线程同步的必要性

举个简单的反例,两个线程同时对全局变量 count 进行自增操作:

#include <iostream>
#include <thread>

using namespace std;

int count = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        count++; // 非原子操作,存在数据竞争
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);
    t1.join();
    t2.join();
    cout << "最终 count 值:" << count << endl;
    return 0;
}

运行该程序会发现,最终 count 的值大概率小于 200000。这是因为 count++ 并非原子操作,被两个线程交替执行打乱了步骤。

C++ 标准库中的互斥锁

C++11 及以后的标准库提供了 <mutex> 头文件,封装了多种互斥锁相关的类。最基础且常用的就是 std::mutex。

std::mutex 的核心接口
  • lock():获取互斥锁。如果锁已被其他线程占用,当前线程会阻塞等待。
  • unlock():释放互斥锁。必须与 lock() 成对使用。
  • try_lock():尝试获取互斥锁。如果获取失败,不会阻塞,直接返回 false。
std::lock_guard:自动管理锁的生命周期

直接使用 lock() 和 unlock() 容易出现遗漏解锁的情况。比如程序抛出异常时,unlock() 可能无法执行,导致死锁。std::lock_guard 基于 RAII 机制实现,可以自动在构造时加锁,析构时解锁。

实际开发中优先使用 std::lock_guard,而非手动调用 lock()/unlock()。

互斥锁实战:解决数据竞争问题

我们使用 std::mutex 和 std::lock_guard 改造上面的反例:

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

int count = 0;
mutex mtx; // 定义全局互斥锁

void increment() {
    for (int i = 0; i < 100000; ++i) {
        lock_guard<mutex> lock(mtx); // 自动加锁
        count++; // 临界区代码,此时只有一个线程能执行
    } // lock_guard 析构,自动解锁
}

int main() {
    thread t1(increment);
    thread t2(increment);
    t1.join();
    t2.join();
    cout << "最终 count 值:" << count << endl;
    return 0;
}

运行该程序,最终 count 的值稳定等于 200000。这说明互斥锁成功保护了临界区代码,避免了数据竞争。

这里涉及两个关键概念:

  1. 临界区:需要被保护的、不能被多个线程同时执行的代码段。上例中 count++ 就是临界区。
  2. 互斥锁的作用:保证同一时刻只有一个线程能进入临界区。

死锁的产生与规避

死锁是指多个线程互相持有对方需要的锁,导致所有线程都无法继续执行的状态。

死锁的四个必要条件
  1. 互斥条件:资源只能被一个线程占用。
  2. 请求与保持条件:线程持有一个资源的同时,请求其他线程持有的资源。
  3. 不可剥夺条件:线程已持有的资源不能被其他线程强制夺走。
  4. 循环等待条件:多个线程形成首尾相接的循环等待资源关系。
死锁的示例

两个线程分别持有一个锁,同时请求对方的锁:

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex mtx1, mtx2;

void thread1() {
    mtx1.lock();
    this_thread::sleep_for(chrono::milliseconds(100)); // 确保 thread2 先拿到 mtx2
    mtx2.lock(); // 等待 mtx2,此时 thread2 持有 mtx2 并等待 mtx1
    cout << "thread1 执行完毕" << endl;
    mtx2.unlock();
    mtx1.unlock();
}

void thread2() {
    mtx2.lock();
    this_thread::sleep_for(chrono::milliseconds(100)); // 确保 thread1 先拿到 mtx1
    mtx1.lock(); // 等待 mtx1,此时 thread1 持有 mtx1 并等待 mtx2
    cout << "thread2 执行完毕" << endl;
    mtx1.unlock();
    mtx2.unlock();
}

int main() {
    thread t1(thread1);
    thread t2(thread2);
    t1.join();
    t2.join();
    return 0;
}

运行该程序,两个线程会互相等待,陷入死锁状态,无法输出任何内容。

规避死锁的常用方法
  1. 固定锁的获取顺序:所有线程按照相同的顺序获取锁。比如上例中,让两个线程都先获取 mtx1,再获取 mtx2。
  2. 使用 std::lock 同时获取多个锁:std::lock 可以一次性获取多个互斥锁,避免循环等待。
  3. 使用带超时的锁尝试:通过 try_lock() 或 std::timed_mutex,在超时后放弃获取锁,避免永久阻塞。

实战案例:多线程售票系统

模拟一个售票系统,多个窗口同时售票,使用互斥锁保证票数不会出现负数或重复售票的情况。

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

using namespace std;

int tickets = 100; // 总票数
mutex mtx;         // 售票函数

void sell_tickets(int window_id) {
    while (true) {
        lock_guard<mutex> lock(mtx);
        if (tickets > 0) {
            cout << "窗口" << window_id << "售出第" << tickets << "张票" << endl;
            tickets--;
            this_thread::sleep_for(chrono::milliseconds(50)); // 模拟售票耗时
        } else {
            break;
        }
    }
    cout << "窗口" << window_id << "售票结束" << endl;
}

int main() {
    vector<thread> windows; // 创建 5 个售票窗口
    for (int i = 1; i <= 5; ++i) {
        windows.emplace_back(sell_tickets, i);
    }
    // 等待所有窗口售票结束
    for (auto& t : windows) {
        t.join();
    }
    cout << "所有票已售罄" << endl;
    return 0;
}

运行效果显示,5 个窗口有序售票,最终票数从 100 递减到 0,不会出现重复售票或票数为负的情况。

本章小结

  1. 多线程访问共享资源时必须进行同步,否则会出现数据竞争问题。
  2. std::mutex 是 C++ 最基础的互斥锁,搭配 std::lock_guard 可以安全地管理锁的生命周期。
  3. 死锁由四个必要条件引发,通过固定锁顺序、使用 std::lock 等方法可以有效规避。
  4. 互斥锁的核心是保护临界区,确保同一时刻只有一个线程能执行临界区代码。

目录

  1. C++ 多线程同步:互斥锁 mutex 实战指南
  2. 多线程同步的必要性
  3. C++ 标准库中的互斥锁
  4. std::mutex 的核心接口
  5. std::lock_guard:自动管理锁的生命周期
  6. 互斥锁实战:解决数据竞争问题
  7. 死锁的产生与规避
  8. 死锁的四个必要条件
  9. 死锁的示例
  10. 规避死锁的常用方法
  11. 实战案例:多线程售票系统
  12. 本章小结

更多推荐文章

查看全部
  • 无需 GPU 的 Retinaface+CurricularFace 轻量级部署方案
  • Git Cherry-Pick 命令用法与场景
  • Java 面试题及答案(208 道)
  • 机器人领域顶级会议梳理与具身智能学习路线
  • 世界模型发展脉络整理:理解世界还是预测未来?
  • 动手学大模型应用开发:从零入门 LLM 开发实战
  • Rust 异步测试与调试实践指南
  • 基于 SpringBoot 的 Java 在线拍卖系统设计与实现
  • 公交网络辅助的无人机调度用于无线可充电传感器网络可持续充电
  • Java Maven 项目结合 Git 与 Jenkins 的自动化构建部署指南
  • 前后端分离架构下的 CORS 跨域配置与解决方案
  • 大模型浪潮:是泡沫还是技术革命?
  • Linux 进程间通信:匿名管道原理与实现
  • Python 毕业设计选题避坑指南:从技术选型到可交付原型
  • 从零手写 C++ 调试器:Windows 平台实现原理与代码
  • Spring Boot 数据导入导出与报表生成实战
  • 现代化Python GUI开发指南:3步打造桌面应用
  • PicGo 结合 GitCode 与 Typora 搭建免费图床指南
  • 基于ROS与Ego-Planner的无人机动态避障实现
  • 从接口文档到前端调用:Axios 封装与实战详解

相关免费在线工具

  • 加密/解密文本

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