cs106l-lecture-6-2-multithreading
Course: 斯坦福大学: C++标准库编程 | C++ Standard Library Programming
🎮 CS106L Lecture 6.2 — Multithreading
🏛 课程:Stanford CS106L — Standard C++ Programming Language
📍 主题:多线程编程(Multithreading in Modern C++)
🎯 目标:理解线程的基本概念、C++11 提供的多线程 API、数据竞争与同步机制,掌握并行执行的正确设计思维。
🧭 课程定位
本讲是 CS106L 课程的收官篇章。
在前几讲我们学习了:
- RAII(资源生命周期管理)
- 智能指针(所有权控制)
而本讲让我们走向“并发”的世界:
💡 “RAII 让资源安全;多线程让资源高效。”
🏷 关键词速记
Thread|线程
Concurrency|并发
Mutex|互斥锁
Data Race|数据竞争
Join / Detach|线程生命周期控制
Atomic|原子操作
Lock Guard|锁的 RAII 封装
Race Condition|竞态条件
Deadlock|死锁
Condition Variable|条件变量
🎮 30 秒课程摘要
“多线程是并行的力量,也是一切灾难的起点。
线程能让程序飞起来,也能让 bug 烧服务器。”
📚 一、为什么需要多线程?
🧩 1️⃣ 单线程的局限性
单线程执行:
downloadFile(); // 等待下载完成
processData(); // 然后再处理
🧠 问题:
- CPU 在等待 I/O 的时候“闲着”
- 用户体验差、响应延迟高
⚙️ 2️⃣ 多线程让任务并行
📦 示例:
std::thread t1(downloadFile);
std::thread t2(processData);
t1.join();
t2.join();🧠 讲解:
- 每个线程独立执行函数体
- 主线程等待两个子线程完成
- 提高资源利用率与响应速度
“并行的本质是让 CPU 在不同核心上同时工作。”
📚 二、std::thread 基本用法
🧩 1️⃣ 启动线程
📦 示例 1:最小线程程序
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(hello); // 启动线程
t.join(); // 等待线程结束
}📤 输出:
Hello from thread!
🧠 关键点:
std::thread构造即启动。.join()会阻塞主线程直到子线程结束。
⚙️ 2️⃣ detach() 与“孤儿线程”
📦 示例 2:detach()
std::thread worker([](){
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Detached thread done\n";
});
worker.detach(); // 主线程不等待
🧠 区别:
| 方法 | 含义 |
|---|---|
.join() | 主线程等待子线程结束 |
.detach() | 子线程独立执行,不再可控 |
.joinable() | 判断线程是否可 join |
⚠️ 注意:
若未调用 join() 或 detach(),程序会抛出异常并终止。
📦 3️⃣ 向线程传参
📦 示例 3:参数传递
void printMsg(const std::string& msg, int n) {
for (int i = 0; i < n; ++i)
std::cout << msg << " ";
}
std::thread t(printMsg, "CS106L", 3);
t.join();📤 输出:
CS106L CS106L CS106L
🧠 要点:
参数会被复制进线程。
若需传引用,应使用
std::ref():std::thread t(func, std::ref(myObj));
📚 三、并发中的危险:数据竞争 (Data Race)
⚠️ 1️⃣ 共享变量的地狱
📦 示例 4:竞争条件
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i)
++counter;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join(); t2.join();
std::cout << counter << "\n"; // ❌ 输出不确定
}🧠 原因:
- 两个线程同时写入
counter。 - CPU 指令层面,
++是多步操作:load → add → store。
📉 结果:
数据竞争导致非确定性行为(Undefined Behavior)。
📚 四、互斥锁(Mutex)
🧩 1️⃣ 使用 std::mutex 保护共享数据
📦 示例 5:加锁修复
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> guard(mtx);
++counter;
}
}🧠 讲解:
std::lock_guard是 RAII 封装:构造加锁 → 析构解锁。- 确保即使抛异常也会自动解锁。
“RAII 让锁安全地离开作用域。”
⚙️ 2️⃣ 死锁与锁顺序问题
📦 错误示例:
std::mutex a, b;
void task1() {
std::lock_guard<std::mutex> lock1(a);
std::lock_guard<std::mutex> lock2(b);
}
void task2() {
std::lock_guard<std::mutex> lock1(b);
std::lock_guard<std::mutex> lock2(a); // ❌ 死锁
}🧠 解决方案:
始终按相同顺序加锁。
或使用
std::scoped_lock(C++17):std::scoped_lock lock(a, b);
📚 五、unique_lock 与条件变量
🧩 1️⃣ unique_lock
std::unique_lock 是更灵活的锁类型(可手动上/解锁)。
📦 示例 6:
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
lock.unlock(); // 手动解锁
lock.lock(); // 再次加锁
🧠 用途:
- 与
condition_variable协作 - 可延迟加锁、提前解锁
🧩 2️⃣ 条件变量(Condition Variable)
📦 示例 7:线程等待通知
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件
std::cout << "Work done!\n";
}
void notifier() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all(); // 唤醒所有等待线程
}🧠 讲解:
cv.wait()会释放锁并挂起线程。- 条件满足 → 自动重新加锁继续执行。
📚 六、原子操作(std::atomic)
⚙️ 无锁线程安全计数器
📦 示例 8:
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i)
++counter; // 原子操作,无需 mutex
}🧠 特点:
- 保证操作不可分割。
- 性能优于加锁。
- 适用于简单数值操作或 flag 检查。
📚 七、线程休眠与调度
📦 示例 9:线程延迟
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Half a second later...\n";🧠 其它控制函数:
| 函数 | 作用 |
|---|---|
sleep_for() | 按时延迟 |
sleep_until() | 睡到指定时间点 |
yield() | 暂时让出 CPU |
get_id() | 获取线程 ID |
📚 八、线程池与并行设计
📦 简易线程池模型
#include <thread>
#include <vector>
void worker(int id) {
std::cout << "Thread " << id << " running\n";
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i)
threads.emplace_back(worker, i);
for (auto& t : threads)
t.join();
}📤 输出(顺序不确定):
Thread 0 running
Thread 2 running
Thread 1 running
Thread 3 running
🧠 说明:
- 多线程输出顺序不确定。
- 可利用多核 CPU 并行执行任务。
📚 九、线程与进程的区别
| 项目 | 线程 | 进程 |
|---|---|---|
| 内存空间 | 共享 | 独立 |
| 通信方式 | 内存共享 | IPC 管道/Socket |
| 创建代价 | 轻 | 重 |
| 崩溃影响 | 会拖累整个进程 | 隔离 |
| C++ 接口 | <thread> | <process> / OS API |
🧠 记忆:
“线程是轻量级并行,进程是重量级隔离。”
🧠 十、数据竞争与同步口诀
| 风险 | 解决方式 |
|---|---|
| 写写冲突 | mutex 或 atomic |
| 读写冲突 | shared_mutex(C++17) |
| 复杂同步 | condition_variable |
| 线程生命周期混乱 | RAII + join() |
| 资源未释放 | RAII + lock_guard |
“加锁要简,作用域要短,锁顺序要统一。”
🧪 十一、实验练习
✅ 练习 1:Hello Threads
启动三个线程,分别输出不同内容。
✅ 练习 2:Data Race 复现
写出竞争版本与加锁修正版。
✅ 练习 3:条件变量
实现一个简单的生产者-消费者队列。
✅ 练习 4:atomic 性能比较
比较 std::mutex 与 std::atomic<int> 的计数速度差异。
🪄 工程与设计建议
- ✅ 始终为线程调用
.join()或.detach() - ✅ 使用 RAII 封装锁 (
std::lock_guard,std::unique_lock) - ✅ 尽量缩小锁的作用范围
- ✅ 避免在锁持有时调用外部函数(易死锁)
- ✅ 能用原子就别用锁
- ✅ 对资源访问加上明确所有权语义
- ✅ 将线程逻辑封装在类中(构造启动,析构 join)
✅ 小结表
| 模块 | 关键概念 | 典型类 |
|---|---|---|
| 线程控制 | 启动 / join / detach | std::thread |
| 同步锁 | 加锁 / 自动解锁 | std::mutex, std::lock_guard |
| 条件同步 | 通知机制 | std::condition_variable |
| 原子操作 | 无锁并发 | std::atomic |
| 高级封装 | RAII 安全 | std::scoped_lock, std::unique_lock |
“线程是力量,锁是秩序,RAII 是守护神。”
📚 参考与延伸
- 📘 C++ Concurrency in Action — Anthony Williams
- 📗 Effective Modern C++ — Scott Meyers
- 📙 The C++ Programming Language (4th Ed.) — Bjarne Stroustrup
- 🧠 cppreference: Thread support library
- 🪄 Stanford CS106L 原版讲义配合 Demo 代码
🎯 终章总结
“C++ 的力量来自于控制:
RAII 控制资源,
智能指针控制所有权,
多线程控制时间。”
🎉 至此,你已掌握现代 C++ 的完整资源管理模型。
是否希望我继续为你整理一个
📘《CS106L 课程全系列总结版(1–6章)》
以 Notion 格式输出(含知识树 + 跨章节对照 + 复习导图)?