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

C++ 多线程进阶:互斥锁原理与实战

多线程共享资源访问易引发竞态条件,导致数据错误如票数变负。通过引入互斥锁(Mutex)可确保临界区同一时刻仅一个线程访问。需正确掌握加锁解锁时机,避免死锁及“持锁休眠”问题。互斥锁保证互斥性但不保证公平性,是并发编程的基础保障。

活在当下发布于 2026/3/20更新于 2026/11/687 浏览
C++ 多线程进阶:互斥锁原理与实战

前言

在编写多线程代码时,我们常遇到乱码或抢占式输出。本章将探讨其成因并给出解决方案。

抢票示例

假设我们有 100 张电影票,5 个线程同时抢票会出现什么情况?我们先尝试写一段代码来看看。

#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 sell ticket, now tickets number: %d\n", name.c_str(), ticket);
        } else {
            std::cout << ticket << std::endl;
            break;
        }
    }
    return;
}

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

这里的公共资源是 ticket,五个线程去抢这个票数。我们用 usleep(1000) 来模拟抢票消耗的时间。按照常理,一旦没票了就应该停止。

但实际运行结果可能会显示票数变成了负数(例如 -4)。为什么会这样?

原因分析

如果是单线程,不会发生这种情况。但在多线程环境下,竞争是常态。

观察代码逻辑:每个线程进入函数后都会读取 ticket 的值,随后休息一秒,再进行减减操作。

让我们放慢过程,详细看看当票数为 1 时的情况:

  1. 线程 1 拿到 ticket 为 1,发现大于 0,执行 usleep(1000)。
  2. 线程 2 启动,此时它可能也读取到 ticket 为 1(因为线程 1 还没写完),同样认为可以减减,也执行 usleep(1000)。
  3. 线程 1 醒来,对 ticket 减减,变成 0。
  4. 线程 2 醒来,基于之前的判断继续减减,导致 ticket 变成 -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:

最关键的一点在于:判断时用的是 R1,真正减法时又重新 LOAD R2, [ticket] 读了一次内存。这意味着两个线程可能在判断阶段都拿到了旧值,导致重复扣减。

引入互斥锁

为了防止上述问题,我们引入锁的概念。先不深究原理,直接看效果。

#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.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 sell ticket, now tickets number: %d\n", name.c_str(), ticket);
            pthread_mutex_unlock(&lock);
        } else {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return;
}

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;
}

这次运行结果正常,票数准确归零。这里的锁就是 互斥锁 (Mutex)。

  • 特点:同一时间只有一个线程能持有锁。
  • 用法:pthread_mutex_lock() 加锁,pthread_mutex_unlock() 解锁。

临界区概念

我们需要上锁是因为线程竞争导致公共资源混乱。访问共享资源(如全局变量、外部文件)的那部分代码段称为 临界区 (Critical Section)。

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

解锁的时机

注意上面的代码中,无论 if 还是 else 分支,我们都进行了解锁。有人可能会问:为什么不像下面这样统一放在最后解锁?

pthread_mutex_lock(&lock);
if (ticket > 0) {
    ticket--;
    printf(...);
    // pthread_mutex_unlock(&lock);
} else {
    // pthread_mutex_unlock(&lock);
    break;
}
pthread_mutex_unlock(&lock);

如果 break 发生在 unlock 之前,且该路径没有解锁,就会导致死锁。这是一种典型的死锁诱因:一个线程在持有锁的情况下直接退出(如 break、return 或异常),而未释放锁,导致其他需要该锁的线程永远等待。

死锁产生的四个必要条件(Coffman 条件)包括互斥、占有并等待、不可剥夺和循环等待。避免死锁的关键之一就是确保所有分支路径都能正确释放资源。

线程拿着锁睡觉

另一个潜在问题是 usleep 的位置。如果在持有锁期间让线程休眠,会阻塞其他所有线程,这是非常低效的。

所以综合下来,程序应该是这样的:

// ... 同上 ...
void routine(std::string name) {
    while (true) {
        pthread_mutex_lock(&lock);
        if (ticket > 0) {
            // 说明可以开始抢票
            ticket--;
            printf("%s sell ticket, now tickets number: %d\n", name.c_str(), ticket);
            pthread_mutex_unlock(&lock);
        } else {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return;
}

公平性问题

即便有了锁,也可能出现某个线程一直抢到票的情况。这并非互斥锁的 bug,而是调度器的行为。互斥锁保证的是互斥性,并不保证公平性。谁先抢到 CPU 和时间片,谁就有机会先获取锁。

总结

多线程环境下共享资源访问易引发竞态条件,导致数据异常如票数变负。通过引入互斥锁(Mutex)可确保临界区同一时刻仅一个线程访问。需正确掌握加锁解锁时机,避免死锁及'持锁休眠'浪费资源。互斥锁保证互斥性但不保证公平性,是并发编程的基础保障。

目录

  1. 前言
  2. 抢票示例
  3. 原因分析
  4. 引入互斥锁
  5. 临界区概念
  6. 解锁的时机
  7. 线程拿着锁睡觉
  8. 公平性问题
  9. 总结

更多推荐文章

查看全部
  • RPA 工程师必备:XPath 表达式实战与避坑指南
  • Open WebUI Docker 部署指南与最佳实践
  • 机器人正运动学与逆运动学基础
  • Java 浮点运算工具类
  • C++中%取余运算符与模运算的区别
  • VSCode 本地部署 DeepSeek,构建私有化 AI 编码助手
  • Python 数据处理进阶:掌握 Filter 函数的高级用法
  • 基于 Web-Check 与内网穿透实现远程站点安全检测
  • JavaScript 基础语法与 jQuery 快速入门
  • Python 在 CentOS 系统上的安装与配置深度指南
  • 基于微信小程序 SSM 的校园顺路代送系统设计与实现
  • FunASR 离线语音转写服务部署与客户端开发实战
  • 汽车雷达多径环境下幽灵目标检测技术解析
  • AI 绘画风格测试指南:5 个实用技巧掌握艺术生成
  • FPGA 流水线优化:盒式滤波与引导滤波架构对比
  • 2026 AI图像生成模型速度对比:谁才是真正的效率王?
  • 2024 国内最新完整 AI 大模型清单及介绍
  • 案例教学:使用 AI 模型解决一道典型的动态规划题
  • 基于 Qwen Image 的儿童专属 AI 绘画工具实战
  • Webnovel Writer:基于 Claude Code 的长篇网文 AI 创作系统

相关免费在线工具

  • 加密/解密文本

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