C++ 多线程与并发系统取向(二)—— 资源保护:std::mutex 与 RAII(类比 Java synchronized)

一、先问一个工程问题

你有一个共享容器:

cpp 复制代码
std::vector<int> data;

两个线程同时往里面 push_back()。

会发生什么?

你可能会说:

vector 不是自动扩容的吗?

是的,但:

vector 不是线程安全的。

内部会:

  • 修改 size
  • 可能 realloc
  • 移动内存

如果两个线程同时写:

未定义行为。

二、资源保护的本质

并发系统里最重要一句话:

不是"给代码加锁",而是"给资源加保护策略"。

资源 = 被多个线程共享的数据。

三、什么是临界区(Critical Section)

临界区是:

访问共享资源的代码区域。

例如:

cpp 复制代码
data.push_back(1);

这一行,就是临界区。

四、最原始写法(危险)

cpp 复制代码
std::mutex mtx;

void task() {
    mtx.lock();
    data.push_back(1);
    mtx.unlock();
}

看起来没问题。

但如果中间抛异常?

cpp 复制代码
mtx.lock();
throw std::runtime_error("error");
mtx.unlock();  // 永远执行不到

锁永远不释放。

其他线程:

永久阻塞(死锁)。

五、C++ 的核心思想:RAII

RAII = Resource Acquisition Is Initialization

意思:

资源的获取与对象生命周期绑定。

锁对象在构造时加锁,在析构时自动解锁。

这就是:

cpp 复制代码
std::lock_guard

六、正确写法:std::lock_guard

cpp 复制代码
std::mutex mtx;

void task() {
    std::lock_guard<std::mutex> lock(mtx);
    data.push_back(1);
}

作用:

  • 构造 → 自动 lock
  • 离开作用域 → 自动 unlock

无论:

  • return
  • 异常
  • break

都会释放锁。

七、Java 对比:synchronized

Java 写法:

cpp 复制代码
synchronized (lock) {
    data.add(1);
}

本质类似:

  • 进入代码块 → 加锁
  • 离开代码块 → 自动释放

区别:

C++ Java
lock_guard 是对象 synchronized 是语言关键字
手动控制 mutex JVM 管理 monitor

八、完整并发示例(安全版)

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

std::vector<int> data;
std::mutex mtx;

void task() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        data.push_back(i);
    }
}

int main() {
    std::thread t1(task);
    std::thread t2(task);

    t1.join();
    t2.join();

    std::cout << data.size() << std::endl;
}

输出:

cpp 复制代码
20000

稳定。

九、锁的粒度问题(工程重点)

你这样写:

cpp 复制代码
void task() {
    std::lock_guard<std::mutex> lock(mtx);
    for (int i = 0; i < 10000; ++i) {
        data.push_back(i);
    }
}

锁范围更大。

优点:

  • 锁竞争少

缺点:

  • 阻塞时间更长

工程原则:

锁只保护"共享资源操作",

不要把耗时操作放在锁里。

十、错误示例:锁太小

cpp 复制代码
if (!data.empty()) {
    std::lock_guard<std::mutex> lock(mtx);
    data.pop_back();
}

问题:

data.empty() 没加锁。

可能:

线程 A 判断非空

线程 B pop 完

线程 A 再 pop → 崩溃

正确写法:

cpp 复制代码
std::lock_guard<std::mutex> lock(mtx);
if (!data.empty()) {
    data.pop_back();
}

十一、资源保护系统思维

写并发代码前问:

1️⃣ 这个数据是否共享?

2️⃣ 是否有写操作?

3️⃣ 锁由谁持有?

4️⃣ 锁范围是否最小化?

十二、死锁风险

死锁发生在:

  • 两个线程
  • 持有不同锁
  • 相互等待

例如:

线程 A:锁 m1 → 锁 m2

线程 B:锁 m2 → 锁 m1

工程规避方式:

统一加锁顺序。

十三、工程 Checklist

✅ 共享数据必须有唯一 mutex

✅ 永远使用 lock_guard(不要手动 lock/unlock)

✅ 判断 + 操作必须在同一锁内

✅ 锁只保护数据,不保护耗时操作


十四、Java 再类比总结

概念 Java C++
自动释放锁 synchronized lock_guard
手动锁 ReentrantLock std::mutex
异常安全 JVM 保证 RAII 保证

十五、本篇总结一句话

线程私有栈,共享堆;

共享必保护;

RAII 是 C++ 锁的灵魂。

下一篇预告

第三篇我们讲:

std::unique_lock ------ 为什么它比 lock_guard 更"重"?

  • 为什么条件变量必须用 unique_lock?
  • 如何避免死锁?
  • defer_lock / try_lock 是干什么的?
  • 锁释放与再获取的工程场景
相关推荐
for_ever_love__12 分钟前
MySQL ORDER BY 排序优化:从 Using filesort 到索引有序扫描(含双路 / 单路排序原理)
java·mysql·优化·存储·by·order·filesort
程序猿追1 小时前
kotlin-retry 接入鸿蒙后,怎样确认它真的会停止重试
开发语言·kotlin·harmonyos
我是人✓1 小时前
Java文件上传三方式对比:从传统到SpringBoot
java·开发语言
智能直播1 小时前
网络RTMP拉流不卡顿、声音不同步,一文讲透缓冲区、时间戳
开发语言·ffmpeg·php·音视频·视频编解码
欣栀♡1 小时前
JVM 频繁 Full GC 先看日志括号里的 cause,jstat 的 FGC 在 JDK 8 上会骗人
java·jvm
IT研究室1 小时前
最新计算机毕业设计选题推荐-基于spring boot的葡萄酒文化宣传网站-网站-文档指导-Java-springboot
java·spring boot·课程设计
慧都小项2 小时前
MyEclipse 中创建和测试 JAX-RS REST 接口:从项目配置到 REST Explorer
java·restful·myeclipse·jax-rs
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 入门案例:编写 Dubbo 服务提供者
java·dubbo·交友
墨云安全2 小时前
内网资产无法暴露到公网,怎么做安全检测?2026 年内网渗透测试的落地路径
开发语言·安全·网络安全·渗透测试·php
秋饼2 小时前
Spring AI 2.0 结构化输出企业级实战:从 BeanOutputConverter 源码到生产级解析容错
java·ai·技术分享·后端开发