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|条件变量


“多线程是并行的力量,也是一切灾难的起点。

线程能让程序飞起来,也能让 bug 烧服务器。”


单线程执行:

downloadFile();   // 等待下载完成
processData();    // 然后再处理

🧠 问题:

  • CPU 在等待 I/O 的时候“闲着”
  • 用户体验差、响应延迟高

📦 示例:

std::thread t1(downloadFile);
std::thread t2(processData);
t1.join();
t2.join();

🧠 讲解:

  • 每个线程独立执行函数体
  • 主线程等待两个子线程完成
  • 提高资源利用率与响应速度

“并行的本质是让 CPU 在不同核心上同时工作。”


📦 示例 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()

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:参数传递

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));

📦 示例 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)。


📦 示例 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 让锁安全地离开作用域。”


📦 错误示例:

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);

std::unique_lock 是更灵活的锁类型(可手动上/解锁)。

📦 示例 6:

std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
lock.unlock();  // 手动解锁
lock.lock();    // 再次加锁

🧠 用途:

  • 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() 会释放锁并挂起线程。
  • 条件满足 → 自动重新加锁继续执行。

📦 示例 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

🧠 记忆:

“线程是轻量级并行,进程是重量级隔离。”


风险解决方式
写写冲突mutexatomic
读写冲突shared_mutex(C++17)
复杂同步condition_variable
线程生命周期混乱RAII + join()
资源未释放RAII + lock_guard

“加锁要简,作用域要短,锁顺序要统一。”


练习 1:Hello Threads

启动三个线程,分别输出不同内容。

练习 2:Data Race 复现

写出竞争版本与加锁修正版。

练习 3:条件变量

实现一个简单的生产者-消费者队列。

练习 4:atomic 性能比较

比较 std::mutexstd::atomic<int> 的计数速度差异。


  • ✅ 始终为线程调用 .join().detach()
  • ✅ 使用 RAII 封装锁 (std::lock_guard, std::unique_lock)
  • ✅ 尽量缩小锁的作用范围
  • ✅ 避免在锁持有时调用外部函数(易死锁)
  • ✅ 能用原子就别用锁
  • ✅ 对资源访问加上明确所有权语义
  • ✅ 将线程逻辑封装在类中(构造启动,析构 join)

模块关键概念典型类
线程控制启动 / join / detachstd::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 格式输出(含知识树 + 跨章节对照 + 复习导图)?