📝 说明:本文为C++学习笔记,AI辅助系统整理
📅 整理时间:2025年10月
🎯 课时:课时06 - 并发编程基础(下篇)
💡 核心内容:条件变量、线程安全类设计、死锁避免、原子操作
难度等级:⭐⭐⭐⭐⭐


在这里插入图片描述

🎯 前言

这是课时06的下半部分,在掌握了基本的线程管理和互斥锁使用后,本文深入学习线程间通信、线程安全类设计、死锁问题以及高性能的原子操作。这些是C++后端开发中必须掌握的核心技能。

上篇回顾

  • std::thread线程基础(join/detach)
  • std::mutex互斥锁与数据竞争
  • std::lock_guard自动锁管理
  • std::unique_lock灵活锁管理

本篇内容

  • std::condition_variable条件变量
  • 线程安全类设计原则
  • 死锁问题与避免策略
  • std::atomic原子操作

📚 本文内容

  1. std::condition_variable条件变量
  2. 线程安全类设计
  3. 死锁问题与避免
  4. std::atomic原子操作

🔔 核心知识点五:std::condition_variable条件变量

为什么需要条件变量?

目前学到的工具

  • mutex:保护共享数据
  • lock_guard/unique_lock:自动管理锁

但是缺少:线程之间如何通信协作


问题场景:生产者-消费者模型

错误的解决方案:忙等待

queue<int> dataQueue;
mutex mtx;

void consumer() {
    for (int i = 0; i < 5; i++) {
        while (true) {
            lock_guard<mutex> lock(mtx);
            
            if (!dataQueue.empty()) {
                int data = dataQueue.front();
                dataQueue.pop();
                cout << "消费: " << data << "\n";
                break;
            }
            
            // ❌ 问题:反复加锁解锁,浪费CPU
        }
    }
}

问题

  • 反复加锁、检查、解锁
  • CPU忙等待,性能很差
  • 需要更好的方法

正确的解决方案:条件变量

#include <condition_variable>
#include <queue>
#include <mutex>
#include <thread>
#include <iostream>
using namespace std;

queue<int> dataQueue;
mutex mtx;
condition_variable cv;  // 条件变量

void producer() {
    for (int i = 0; i < 5; i++) {
        {
            lock_guard<mutex> lock(mtx);
            dataQueue.push(i);
            cout << "生产: " << i << "\n";
        }  // 解锁
        
        cv.notify_one();  // 通知一个等待的线程
        
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

void consumer() {
    for (int i = 0; i < 5; i++) {
        unique_lock<mutex> lock(mtx);  // 必须用unique_lock
        
        // 等待队列非空
        cv.wait(lock, []{ return !dataQueue.empty(); });
        
        int data = dataQueue.front();
        dataQueue.pop();
        cout << "消费: " << data << "\n";
    }
}

int main() {
    thread t1(producer);
    thread t2(consumer);
    
    t1.join();
    t2.join();
    
    return 0;
}

输出

生产: 0
消费: 0
生产: 1
消费: 1
生产: 2
消费: 2
生产: 3
消费: 3
生产: 4
消费: 4

完美!生产一个,消费一个!


cv.wait()工作原理详解

这是学习过程中的重点问题!

cv.wait(lock, []{ return !dataQueue.empty(); });

这行代码做了什么?

步骤1:检查条件(lambda表达式)
[]{ return !dataQueue.empty(); }  // 条件:队列非空

如果条件为true

  • wait立即返回,继续执行

如果条件为false

  • 进入步骤2

步骤2:释放锁并等待
// wait内部的伪代码逻辑
void wait(unique_lock& lock, Predicate pred) {
    while (!pred()) {  // 条件不满足
        lock.unlock();  // 🔓 自动释放锁!(重要)
        
        // 线程进入等待状态(不占CPU)
        // 等待其他线程调用notify_one()或notify_all()
        
        // 被唤醒后
        lock.lock();    // 🔒 重新获取锁
        
        // 再次检查条件(防止虚假唤醒)
    }
    // 条件满足,返回
}

关键理解

  1. wait会自动unlock():让其他线程(生产者)能获取锁
  2. 线程进入睡眠:不占CPU,等待被唤醒
  3. 被唤醒后重新lock():再次获取锁
  4. 重新检查条件:防止虚假唤醒

条件变量工作流程图解

生产者 共享队列 条件变量 消费者 检查队列 队列为空 wait(lock, 队列非空?) unlock + 睡眠 push(data) notify_one() 唤醒 lock + 检查条件 pop(data) 消费数据 生产者 共享队列 条件变量 消费者

这个图展示了条件变量的完整工作流程!


notify_one() vs notify_all()

notify_one():唤醒一个等待的线程
cv.notify_one();  // 只唤醒一个线程

使用场景

  • 只需要一个线程处理
  • 例如:生产一个数据,只需要一个消费者

notify_all():唤醒所有等待的线程
cv.notify_all();  // 唤醒所有等待的线程

使用场景

  • 需要所有线程都知道
  • 例如:程序退出信号,所有线程都要停止

为什么notify可以在锁外调用?

学习过程中的问题:为什么推荐在锁外notify?

在锁内notify(可能低效)
void producer() {
    lock_guard<mutex> lock(mtx);  // 上锁
    dataQueue.push(data);
    cv.notify_one();  // 在锁内通知
}  // 这里才解锁

执行过程

1. 生产者持有锁
2. 生产者notify_one()
3. 消费者被唤醒
4. 消费者尝试lock() → ❌ 阻塞!因为生产者还没解锁
5. 生产者离开作用域,解锁
6. 消费者获得锁,继续执行

问题:消费者醒来后立即又阻塞等锁


在锁外notify(推荐)
void producer() {
    {
        lock_guard<mutex> lock(mtx);
        dataQueue.push(data);
    }  // 先解锁
    
    cv.notify_one();  // 在锁外通知
}

执行过程

1. 生产者持有锁
2. 生产者离开作用域,解锁
3. 生产者notify_one()
4. 消费者被唤醒
5. 消费者尝试lock() → ✅ 成功!锁已释放
6. 消费者继续执行

优势:消费者醒来后直接获得锁,避免二次阻塞


为什么wait必须用unique_lock?

wait的工作需要

lock.unlock();  // 释放锁
// 等待...
lock.lock();    // 重新获取锁

lock_guard不能用

  • ❌ lock_guard没有unlock()方法
  • ❌ 不支持手动unlock和lock

unique_lock可以用

  • ✅ unique_lock有unlock()和lock()方法
  • ✅ wait内部可以操作锁

这就是为什么条件变量必须配合unique_lock!


虚假唤醒问题

什么是虚假唤醒?

线程可能在没有notify的情况下被唤醒(操作系统行为)

错误的写法

// ❌ 危险!可能虚假唤醒
cv.wait(lock);  // 没有条件检查

// 醒来后,队列可能还是空的!
int data = dataQueue.front();  // 崩溃!

正确的写法

// ✅ 安全!带条件检查
cv.wait(lock, []{ return !dataQueue.empty(); });

// wait内部会循环检查条件
// while (!条件) { 等待... }
// 即使虚假唤醒,也会重新检查条件

条件变量的核心要点

  1. 必须配合mutex使用
  2. 必须用unique_lock(不能用lock_guard)
  3. 必须有条件检查(lambda表达式)
  4. notify可以在锁外调用(推荐,避免唤醒后立即阻塞)

🛡️ 核心知识点六:线程安全类设计

什么是线程安全的类?

线程安全:多个线程同时使用这个类的对象,不会出现数据竞争或错误结果


线程不安全的计数器类

class Counter {
private:
    int count;
    
public:
    Counter() : count(0) {}
    
    void increment() {
        count++;  // ❌ 数据竞争!
    }
    
    int get() {
        return count;  // ❌ 读写冲突!
    }
};

// 使用
Counter counter;

void worker() {
    for (int i = 0; i < 100000; i++) {
        counter.increment();
    }
}

int main() {
    thread t1(worker);
    thread t2(worker);
    
    t1.join();
    t2.join();
    
    cout << counter.get() << "\n";  // 期望200000,实际可能150000
    return 0;
}

线程安全的计数器类

class ThreadSafeCounter {
private:
    int count;
    mutex mtx;  // 成员变量mutex
    
public:
    ThreadSafeCounter() : count(0) {}
    
    void increment() {
        lock_guard<mutex> lock(mtx);  // 保护count
        count++;
    }
    
    int get() {
        lock_guard<mutex> lock(mtx);  // 读也要保护
        return count;
    }
    
    void reset() {
        lock_guard<mutex> lock(mtx);
        count = 0;
    }
};

使用

ThreadSafeCounter counter;

void worker() {
    for (int i = 0; i < 100000; i++) {
        counter.increment();  // 线程安全
    }
}

int main() {
    thread t1(worker);
    thread t2(worker);
    
    t1.join();
    t2.join();
    
    cout << counter.get() << "\n";  // 一定是200000
    return 0;
}

输出200000 ✅ 正确!


设计线程安全类的核心原则

原则1:保护所有共享数据
class BankAccount {
private:
    int balance;
    string owner;
    mutex mtx;  // 一个mutex保护所有数据
    
public:
    void deposit(int amount) {
        lock_guard<mutex> lock(mtx);
        balance += amount;  // 修改balance需要锁
    }
    
    int getBalance() {
        lock_guard<mutex> lock(mtx);
        return balance;  // 读取balance也需要锁
    }
    
    string getOwner() {
        lock_guard<mutex> lock(mtx);
        return owner;  // 读取owner也需要锁
    }
};

关键

  • 所有成员变量都要保护
  • 读操作也要加锁(避免读写冲突)

原则2:不要返回内部数据的引用或指针

学习过程中的问题:为什么返回引用不安全?

❌ 不安全:返回引用

class UserManager {
private:
    vector<string> users;
    mutex mtx;
    
public:
    // ❌ 危险!返回引用
    vector<string>& getUsers() {
        lock_guard<mutex> lock(mtx);
        return users;  // 返回引用
    }  // 🔓 锁在这里释放了!
};

// 使用
vector<string>& userList = manager.getUsers();
// 💀 危险!现在没有锁保护了
userList.push_back("hacker");  // 其他线程可能同时在修改users

问题

  • 锁只保护了getUsers()函数内部
  • 返回引用后,外部可以随意修改,没有锁保护
  • 其他线程可能同时访问 → 数据竞争

✅ 安全方案1:返回副本

vector<string> getUsers() {
    lock_guard<mutex> lock(mtx);
    return users;  // 返回副本,安全
}

✅ 安全方案2:提供安全的访问接口

void forEachUser(function<void(const string&)> func) {
    lock_guard<mutex> lock(mtx);  // 🔒 上锁
    for (const auto& user : users) {
        func(user);  // 在锁的保护下访问
    }
}  // 🔓 所有访问结束后才解锁

为什么安全?

  • 整个访问过程都在锁的保护下
  • 外部无法拿到users的引用
  • 其他线程必须等待 → 没有数据竞争

关键锁要保护整个访问过程,不能只保护一半


线程安全类设计Checklist

✅ 1. 所有成员变量都有mutex保护
✅ 2. 所有public成员函数都加锁
✅ 3. 读操作也要加锁
✅ 4. 不返回内部数据的引用/指针
✅ 5. 原子操作在一个锁内完成
✅ 6. mutex声明为mutable(如果有const函数)

🔒 核心知识点七:死锁问题与避免

什么是死锁?

死锁:两个或多个线程互相等待对方释放锁,导致都无法继续执行


死锁示例

mutex mtx1;  // 账户1的锁
mutex mtx2;  // 账户2的锁

// 线程1:A转账给B
void transfer_A_to_B() {
    mtx1.lock();  // 🔒 锁住账户A
    cout << "线程1:锁住A\n";
    
    this_thread::sleep_for(chrono::milliseconds(100));
    
    mtx2.lock();  // 等待锁住账户B
    cout << "线程1:锁住B\n";
    
    // 转账操作
    
    mtx2.unlock();
    mtx1.unlock();
}

// 线程2:B转账给A
void transfer_B_to_A() {
    mtx2.lock();  // 🔒 锁住账户B
    cout << "线程2:锁住B\n";
    
    this_thread::sleep_for(chrono::milliseconds(100));
    
    mtx1.lock();  // 等待锁住账户A
    cout << "线程2:锁住A\n";
    
    // 转账操作
    
    mtx1.unlock();
    mtx2.unlock();
}

死锁发生过程

时刻1:线程1锁住mtx1 🔒
时刻2:线程2锁住mtx2 🔒
时刻3:线程1想锁mtx2,但被线程2占用 → 等待...
时刻4:线程2想锁mtx1,但被线程1占用 → 等待...

结果:两个线程互相等待,永远卡住!💀

运行结果

线程1:锁住A
线程2:锁住B
(程序卡死,不再输出)

死锁发生过程图解

线程1 mutex1 mutex2 线程2 lock() 🔒 线程1持有 lock() 🔒 线程2持有 lock() 尝试 等待M2... lock() 尝试 等待M1... 💀 死锁!互相等待 线程1 mutex1 mutex2 线程2

避免死锁的方法

方法1:固定加锁顺序

死锁的原因:两个线程以不同顺序加锁

线程1:先锁mtx1,再锁mtx2
线程2:先锁mtx2,再锁mtx1  // 顺序相反!

解决统一加锁顺序

// ✅ 线程1:固定顺序
void thread1() {
    mtx1.lock();  // 先锁1
    mtx2.lock();  // 再锁2
    
    // 操作
    
    mtx2.unlock();
    mtx1.unlock();
}

// ✅ 线程2:相同顺序
void thread2() {
    mtx1.lock();  // 先锁1(和线程1一样)
    mtx2.lock();  // 再锁2
    
    // 操作
    
    mtx2.unlock();
    mtx1.unlock();
}

结果:不会死锁!

原因

  • 两个线程都先抢mtx1
  • 谁先抢到mtx1,谁就能继续抢mtx2
  • 另一个线程等待mtx1释放
  • 不会互相等待

方法2:使用std::lock()同时加锁

学习过程中的问题mtx.lock() vs std::lock()的区别?

mtx.lock():单独锁一个mutex

mtx1.lock();  // 只锁mtx1
mtx2.lock();  // 再锁mtx2
// 可能死锁

std::lock():同时锁多个mutex,避免死锁

#include <mutex>

mutex mtx1;
mutex mtx2;

void safeTransfer() {
    // 同时锁住mtx1和mtx2,保证不死锁
    std::lock(mtx1, mtx2);
    
    // 用adopt_lock告诉lock_guard:锁已经获取了,不要再lock
    lock_guard<mutex> lock1(mtx1, adopt_lock);
    lock_guard<mutex> lock2(mtx2, adopt_lock);
    
    // 操作
    cout << "安全转账\n";
    
    // 自动解锁
}

std::lock()的特点

  • C++11提供的函数
  • 一次锁多个mutex
  • 自动避免死锁
  • 保证:要么全部锁住,要么全部不锁

std::lock()避免死锁的原理图解

std::lock mtx1, mtx2
能同时锁住?
全部锁住
全部释放
等待一会
lock_guard接管adopt_lock
执行操作
自动unlock

关键:要么全成功,要么全失败,避免部分锁住导致死锁


adopt_lock是什么?

学习过程中的问题:adopt_lock干嘛的?为什么又用lock_guard?

adopt_lock 是一个标记(C++11),告诉lock_guard锁已经lock了,不要再lock

为什么需要adopt_lock?

问题:std::lock()后还要用lock_guard管理解锁

std::lock(mtx1, mtx2);  // 已经锁住了

// 但是还要管理解锁!
// 如果中间抛异常,需要手动unlock,很麻烦

mtx1.unlock();  // 可能忘记
mtx2.unlock();
解决:用lock_guard自动管理解锁
std::lock(mtx1, mtx2);  // 手动lock

// 用lock_guard管理解锁,但告诉它"已经lock过了"
lock_guard<mutex> lock1(mtx1, adopt_lock);  // adopt_lock = 接管已有的锁
lock_guard<mutex> lock2(mtx2, adopt_lock);

// 操作...

// lock_guard析构时会自动unlock
对比:不用adopt_lock会怎样?
std::lock(mtx1, mtx2);  // 已经lock了

lock_guard<mutex> lock1(mtx1);  // ❌ lock_guard会再次lock!
// 但mtx1已经被lock了 → 死锁!
完整流程
// 步骤1:std::lock同时锁住
std::lock(mtx1, mtx2);
// mtx1: 🔒 已锁
// mtx2: 🔒 已锁

// 步骤2:lock_guard接管(adopt_lock)
lock_guard<mutex> lock1(mtx1, adopt_lock);  // 不会再lock,只负责unlock
lock_guard<mutex> lock2(mtx2, adopt_lock);

// 步骤3:操作
cout << "安全操作\n";

// 步骤4:自动解锁(lock_guard析构)
// lock1析构 → mtx1.unlock() 🔓
// lock2析构 → mtx2.unlock() 🔓

总结对比

概念 作用 是否C++11
mtx.lock() 单独锁一个mutex 不是(C++基础)
std::lock() 同时锁多个mutex,避免死锁 ✅ C++11
adopt_lock 告诉lock_guard不要lock,只管unlock ✅ C++11

一句话总结

std::lock(mtx1, mtx2);  // 手动lock(避免死锁)
lock_guard<mutex> lock1(mtx1, adopt_lock);  // 自动unlock(异常安全)

// = 手动lock的安全性 + 自动unlock的便利性

核心知识点八:std::atomic原子操作

为什么需要atomic?

之前的方法

int counter = 0;
mutex mtx;

void increment() {
    lock_guard<mutex> lock(mtx);  // 加锁
    counter++;
    // 解锁
}

问题

  • 只是counter++这么简单的操作
  • 却要加锁解锁,开销大
  • 能不能更高效?

atomic的解决方案

#include <atomic>
#include <thread>
#include <iostream>
using namespace std;

atomic<int> counter(0);  // 原子变量,不需要mutex

void increment() {
    counter++;  // 原子操作,不需要加锁!
}

int main() {
    thread t1(increment);
    thread t2(increment);
    
    t1.join();
    t2.join();
    
    cout << counter << "\n";  // 输出:2,正确!
    return 0;
}

特点

  • ✅ 不需要mutex
  • ✅ 不需要加锁
  • counter++原子操作,硬件级别保证
  • ✅ 性能更好

atomic的基本用法

#include <atomic>

// 创建atomic变量
atomic<int> count(0);        // 原子int
atomic<bool> flag(false);    // 原子bool
atomic<double> value(3.14);  // 原子double

// 基本操作
int val = count;      // 读取
count = 10;           // 写入
count++;              // 自增(原子操作)
count--;              // 自减
count += 5;           // 加法

atomic vs mutex性能对比

用mutex的版本
int counter = 0;
mutex mtx;

void addCount() {
    for (int i = 0; i < 1000000; i++) {
        lock_guard<mutex> lock(mtx);  // 🔒 加锁
        counter++;
        // 🔓 解锁
    }
}
// 100万次加锁解锁!

耗时:约 200ms


用atomic的版本
atomic<int> counter(0);

void addCount() {
    for (int i = 0; i < 1000000; i++) {
        counter++;  // 无锁,硬件原子操作
    }
}
// 100万次原子操作

耗时:约 20ms

快了10倍!


atomic vs mutex性能对比图解

graph LR
    subgraph atomic操作
        A1[counter++] --> A2[硬件原子指令]
        A2 --> A3[耗时: 20ns]
    end
    
    subgraph mutex操作
        M1[lock] --> M2[counter++]
        M2 --> M3[unlock]
        M3 --> M4[耗时: 200ns]
    end
    
    C{选择?} --> |简单操作| atomic操作
    C --> |复杂逻辑| mutex操作
    
    style A3 fill:#90EE90
    style M4 fill:#FFD700

atomic适合的场景

✅ 适合atomic的场景
// 1. 简单计数器
atomic<int> requestCount(0);
requestCount++;

// 2. 标志位
atomic<bool> stopFlag(false);
if (stopFlag) { /* 停止 */ }

// 3. 简单的状态
atomic<int> status(0);
status = 1;  // 改变状态

特点

  • 简单的读写操作
  • 不需要复杂的逻辑
  • 高性能要求

❌ 不适合atomic的场景
// ❌ 复杂操作:需要多个步骤
atomic<int> balance(1000);

// 这个不是原子的!
if (balance >= amount) {      // 步骤1:检查
    balance -= amount;        // 步骤2:扣款
}
// 两个步骤之间,其他线程可能修改balance

// 正确做法:用mutex
mutex mtx;
lock_guard<mutex> lock(mtx);
if (balance >= amount) {
    balance -= amount;
}

特点

  • 需要多步操作
  • 有复杂判断
  • 要用mutex

atomic vs mutex对比

特性 atomic mutex
性能 快(硬件级别) 慢(系统调用)
适用场景 简单操作(++、=) 复杂操作
能否保护多个变量 ❌ 只能一个 ✅ 可以多个
能否加条件判断 ❌ 复杂判断不行 ✅ 可以
使用难度 简单 需要小心死锁

选择原则

  • atomic:简单计数、标志位 → 高性能
  • mutex:复杂逻辑、多变量 → 功能强大

🎯 重要问题解答

学习过程中的关键问题

Q1: cv.wait(lock, []{ return !dataQueue.empty(); })这句代码如何理解?

A: 这行代码做了三件事:

  1. 检查条件:lambda表达式!dataQueue.empty()返回true吗?
  2. 如果false:自动unlock()释放锁,线程睡眠等待
  3. 被唤醒后:重新lock(),再次检查条件,如果true则返回

关键:wait内部会循环检查条件,防止虚假唤醒

Q2: 为什么在锁外notify可以避免唤醒后立即阻塞?

A:

  • 锁内notify:生产者还持有锁 → 消费者被唤醒 → 立即阻塞等锁
  • 锁外notify:生产者已释放锁 → 消费者被唤醒 → 直接获得锁
  • 避免了一次不必要的阻塞等待
Q3: 为什么wait必须用unique_lock而不能用lock_guard?

A:
因为wait内部需要:

  • lock.unlock() 释放锁
  • lock.lock() 重新获取锁

lock_guard没有这两个方法,unique_lock有。

Q4: 返回引用为什么不安全?如何理解"安全的访问接口"?

A:

  • 不安全原因:返回引用后,锁已释放,外部可以随意修改,没有锁保护
  • 安全的接口:整个访问过程都在锁的保护下,外部无法拿到引用
  • 关键:锁要保护整个访问过程,不能只保护一半
Q5: function<void(const string&)>是什么语法?

A:
这是C++11的std::function,用于存储可调用对象:

  • 在课时04学过
  • 可以存储普通函数、lambda表达式、函数对象
  • 常用于传递lambda作为参数
Q6: mtx.lock() vs std::lock()的区别?

A:

  • mtx.lock():单独锁一个mutex,可能死锁
  • std::lock():同时锁多个mutex,自动避免死锁(C++11)
  • std::lock()保证:要么全部锁住,要么全部不锁
Q7: adopt_lock是什么?为什么需要它?

A:

  • adopt_lock是C++11的标记,告诉lock_guard"锁已经lock了,不要再lock"
  • 用途:配合std::lock()使用
    • std::lock()负责安全地加锁(避免死锁)
    • lock_guard负责自动解锁(异常安全)
  • 结果:手动lock的安全性 + 自动unlock的便利性
Q8: atomic适合什么场景?什么场景必须用mutex?

A:

  • atomic适合:简单的++、–、赋值等单个操作
  • mutex适合:复杂的多步操作、条件判断、保护多个变量
  • 选择原则:简单操作优先atomic(性能好),复杂逻辑用mutex(功能全)

📝 我的理解

今天学习的收获

1. cv.wait()这行代码终于搞懂了

一开始看到cv.wait(lock, []{ return !dataQueue.empty(); });直接懵了:

  • 这啥语法啊,lambda表达式咋用的
  • 后来知道wait会自动unlock释放锁,线程睡眠
  • 被唤醒后重新lock,再检查条件
  • 这个设计太巧妙了,避免了忙等待浪费CPU

关键是理解了wait内部的循环检查,防止虚假唤醒:

while (!条件) {
    unlock();  // 释放锁
    等待...    // 睡眠
    lock();    // 重新获取锁
}

2. 在锁外notify的原因

这个当时有点困惑,后来理解了:

  • 锁内notify:消费者醒来 → 立即阻塞等锁 → 低效
  • 锁外notify:先解锁 → 再唤醒 → 消费者直接获得锁 → 高效
  • 避免了"唤醒后立即阻塞"的二次等待

3. function<void(const string&)>这个语法

这个语法之前没见过,一脸懵:

  • 后来查了一下,原来是C++11的std::function
  • 可以存储任何可调用对象(函数、lambda、函数对象)
  • 在课时04学过,但当时没太注意
  • 现在知道了,常用于传递lambda作为参数

4. 返回引用为啥不安全终于理解了

之前一直以为返回引用没问题,今天才发现大问题:

  • 返回引用后,锁已经释放了
  • 外部拿到引用,可以随意修改,没有锁保护
  • 其他线程可能同时访问 → 数据竞争

关键理解锁要保护整个访问过程,不能只保护一半

安全的做法:

  • 要么返回副本
  • 要么提供安全的访问接口(整个访问过程都在锁内)

5. 死锁的本质和解决方法

死锁这个概念之前听说过,但没深入理解:

  • 两个线程互相等对方释放锁 → 永远卡住
  • 原因:加锁顺序不一致

解决方法学了两个:

  • 方法1:固定加锁顺序(简单粗暴)
  • 方法2:用std::lock()同时加锁(更安全)

6. std::lock()和adopt_lock的配合

这个当时完全没懂,后来反复看文档才理解:

  • std::lock(mtx1, mtx2):手动加锁,避免死锁
  • lock_guard<mutex> lock(mtx1, adopt_lock):自动解锁,异常安全
  • adopt_lock就是告诉lock_guard"别再lock了,已经lock过了"

组合拳:手动lock的安全性 + 自动unlock的便利性

7. atomic的性能优势

这个真是太爽了:

  • 简单的counter++,用mutex要100ms
  • 用atomic只要10ms,快了10倍!
  • 原因:硬件级别的原子操作,不需要系统调用

但是atomic只适合简单操作:

  • 复杂的多步骤操作还是要用mutex
  • 不能盲目用atomic

还不太理解的地方

1. 虚假唤醒的具体场景

虽然知道要防止虚假唤醒,但具体啥情况下会虚假唤醒还是不太清楚:

  • 操作系统的行为?
  • 实际项目中会经常遇到吗?
  • 需要实际踩坑才能体会

2. mutable mutex是啥意思

线程安全类设计checklist里提到"mutex声明为mutable":

  • mutable是啥关键字?
  • 为啥mutex要声明为mutable?
  • 跟const函数有关系吗?
  • 需要进一步学习

实际应用的思考

1. 生产者-消费者模型太实用了

这个模型感觉在很多场景都能用:

  • 消息队列
  • 任务分发
  • 数据缓冲

需要多写几遍代码加深理解

2. 线程安全类设计的checklist很重要

这次学到了设计线程安全类的原则:

  • 所有成员变量都要mutex保护
  • 所有public函数都要加锁
  • 读操作也要加锁
  • 不返回引用/指针
  • 原子操作在一个锁内完成

感觉这个checklist以后写代码时要经常参考

3. 死锁问题要时刻警惕

虽然学了避免死锁的方法,但感觉实际项目中还是容易踩坑:

  • 加锁顺序可能不好控制
  • 多个mutex容易乱
  • 需要养成好习惯:能用std::lock就用

4. atomic vs mutex的选择

现在有了新的工具atomic,但要知道什么时候用:

  • 简单场景:计数器、标志位 → atomic(性能好)
  • 复杂场景:多步操作、条件判断 → mutex(功能全)

不能为了性能盲目用atomic,安全第一

整体感受

课时06的下半部分难度明显比上半部分高:

  • 条件变量的wait机制很复杂
  • 线程安全类设计要考虑很多细节
  • 死锁问题需要深入理解
  • atomic虽然简单但要知道适用场景

感觉并发编程的水很深,这才刚入门。需要多写代码、多踩坑、多总结经验才能真正掌握。


💡 学习建议:条件变量和线程安全类设计是并发编程的核心,建议多写生产者-消费者模型的练习代码,加深理解。死锁问题要特别注意,实际项目中要始终保持警惕。

🎯 下一步:建议开始做一些综合练习,比如实现线程池、线程安全的消息队列等,将所学知识综合应用。


📝 本文记录了C++并发编程高级主题的学习过程,从条件变量到原子操作,掌握了线程间通信和高性能并发编程的核心技能。记住:正确的并发编程需要深入理解每个工具的工作原理和适用场景!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐