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 是干什么的?
  • 锁释放与再获取的工程场景
相关推荐
智者知已应修善业18 分钟前
【51单片机按键调节占空比3位数码管显示】2023-8-24
c++·经验分享·笔记·算法·51单片机
韦禾水26 分钟前
记录一次项目部署到tomcat的异常
java·tomcat
曦月合一35 分钟前
树莓派安装jdk、tomcat、vnc、谷歌浏览器开机自启等环境配置
java·tomcat·树莓派
harder3211 小时前
RMP模式的创新突破
开发语言·学习·ios·swift·策略模式
jinanwuhuaguo1 小时前
OpenClaw工程解剖——RAG、向量织构与“记忆宫殿”的索引拓扑学(第十三篇)
android·开发语言·人工智能·kotlin·拓扑学·openclaw
Rust研习社1 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
此剑之势丶愈斩愈烈1 小时前
openssl 自建证书
java
面汤放盐1 小时前
何时使用以及何时不应使用微服务:没有银弹
java·运维·云计算
0xDevNull1 小时前
Spring Boot 自动装配:从原理到实践
java·spring boot·后端