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

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

多线程编程中共享资源访问易引发竞态条件,导致数据错误如票数变负。通过引入互斥锁可确保同一时刻仅一个线程进入临界区,保护共享变量。需合理设置加锁解锁时机,避免死锁及持锁休眠问题。互斥锁保证互斥性但不保证公平性,理解临界区与锁粒度是编写安全并发程序的关键。

古灵精怪发布于 2026/3/16更新于 2026/5/2718 浏览
C++ 多线程进阶:互斥锁解决竞态条件

1. 前言

在我们之前学习的代码中,构建多线程时可能会出现乱码或抢占输出。本章将探讨其原因。

  1. 分析示例问题所在。
  2. 理解线程互斥概念。
  3. 认识互斥锁机制。
  4. 掌握互斥锁使用方法。

2. 买票示例

假设有 100 张电影票,同时抢票会出现什么问题?

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

int ticket = 100;

void routine(std::string name) {
    while (1) {
        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.emplace_back(routine, name);
    }
    for (auto& thread : threads) {
        thread.join();
    }
    return 0;
}

公共资源 ticket 被五个线程竞争。使用 usleep(1000) 模拟抢票耗时。理论上票数为 0 时应停止,但运行结果可能为负数。

2-1 原因

单线程不会发生此问题,多线程存在竞争。每个线程读取 ticket 值后休息,再减减。

当票数为 1 时:线程 1 读取到 1,休息;线程 2 启动,也读取到 1,休息。线程 1 醒来执行减减变为 0。线程 2 醒来基于之前的判断继续减减,变为 -1。

这是经典的 check-then-act race(检查后执行竞态)。汇编层面表现为:判断时加载寄存器 R1,减法时重新加载内存到 R2,导致数据不一致。

3. 引入锁的概念

为防止上述问题,引入锁机制。

#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 (1) {
        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() 解锁。

3-1 互斥锁上锁的位置

访问公共资源的地方都需要上锁,即临界区 (Critical Section)。

  • 定义: 代码中访问共享资源(全局变量、外部文件等)的部分。
  • 规则: 同一时刻只允许一个线程进入。
  • 后果: 不保护会发生'竞态条件',导致数据毁坏。
  • 方式: 进入前加锁,离开后解锁。

3-2 解锁的时机

代码中无论 if 还是 else 都要解锁。若直接 break 而不解锁,会导致死锁。

  • 死锁诱因: 线程持有锁退出(break/return/异常),未释放锁,其他线程永久等待。
  • Coffman 条件: 互斥、占有并等待、不可剥夺、循环等待。

3-3 线程拿着锁睡觉

usleep 不应在锁内执行,否则浪费 CPU 资源给其他线程。

优化后的代码应将 usleep 移出临界区,或仅在获取锁后短暂操作。

// 优化建议:usleep 放在锁外,或仅在必要时休眠
void routine(std::string name) {
    while (1) {
        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;
}

3-4 公平性问题

即使有锁,也可能出现某个线程反复抢到锁的情况(如 thread-2)。互斥锁保证互斥,但不保证公平调度。

4. 总结

本文从票数变负的现象出发,分析了多线程中的竞态条件(Race Condition)。核心原因是 check-then-act 逻辑下,线程间对共享资源的非原子访问。通过互斥锁(Mutex)可以确保临界区的互斥访问,防止数据错乱。使用时需注意锁的范围,避免包含耗时操作(如 sleep),并确保所有分支路径都能正确解锁以防死锁。此外,互斥锁不提供公平性保障,线程调度仍取决于操作系统。掌握临界区管理与锁粒度控制是编写安全并发程序的基础。

目录

  1. 1. 前言
  2. 2. 买票示例
  3. 2-1 原因
  4. 3. 引入锁的概念
  5. 3-1 互斥锁上锁的位置
  6. 3-2 解锁的时机
  7. 3-3 线程拿着锁睡觉
  8. 3-4 公平性问题
  9. 4. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • SD-Trainer 快速上手:AI 绘画模型微调实战
  • Spring AOP 核心概念与实战应用详解
  • Helm 原理与实战:私有库搭建及 UI 工具概览
  • AIGC 个性化与定制化内容生成:技术原理与应用实践
  • C++ 高并发内存池实战:ThreadCache 设计与实现
  • DeepSeek R1 实测:物理模拟能力与开源模型影响分析
  • 网络安全自学指南:零基础入门与学习路线
  • NativeScript-Vue 实战:用 Vue 语法构建高性能原生应用
  • Java 核心面试题与参考答案整理
  • 单链表核心操作全实现与深度解析
  • 利用 AI 构建小红书卡片 MCP 工具实战
  • Python 微服务分布式追踪实战:基于 OpenTelemetry 的全链路监控
  • 算法优选:位运算实战技巧与经典例题解析
  • EFSI-DETR:无人机图像实时小目标检测的高效频域 - 语义集成方法
  • LLaMA3:开源战胜闭源意味着什么
  • Ubuntu 20.04 Docker 安装与核心使用指南
  • 基于 DeepFace 和 OpenCV 的实时情绪分析器
  • 中国 AIGC 应用全景图谱发布及 2024 值得关注的企业产品榜单
  • ToDesk、顺网云与海马云部署 DeepSeek 实测对比
  • 栈的压入弹出序列判断算法详解

相关免费在线工具

  • 加密/解密文本

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