Day1–Day5 我已经把并发的基本组件跑通了:线程、互斥锁、条件变量、future 等。但真正让我感觉“学会了”的,不是继续加新知识点,而是把几个经典题目重新写一遍:写的时候会暴露出很多之前“似懂非懂”的细节——尤其是生命周期、等待条件(predicate)、退出语义、以及 stop 场景下的行为。
因此 Day6 我做了三件事:
- 回顾 Day1–Day5 的知识主线,提炼出一个通用的入手模板
- 复写三个经典并发题:SPSC 阻塞队列 / 写者优先 RWLock / 带返回值线程池
- 记录复写过程中遇到的坑,以及修正后的“可解释版本”
一、Day1–Day5 回顾:我到底在学什么
这 5 天看似做了很多题,但本质都在训练同一件事:
在多线程环境下,把“共享状态”变得可控:谁能改、何时能改、何时等待、何时唤醒、何时退出。
我现在用一句话概括每一天的核心:
- Day1:线程与执行流(
std::thread的基本使用、join/生命周期意识) - Day2:条件变量的“等待条件” (
cv.wait(lock, predicate)抵抗虚假唤醒) - Day3:生产者-消费者(共享队列 + 两类等待条件 + 正确唤醒对象)
- Day4:读写锁与公平性(写者优先的策略:只要有写者等待,读者就不再进入)
- Day5:future/超时/健壮性(
wait_for、可控等待、结果收集)
这一套知识最后都会落到一个通用模板上:
我的并发“通用入手模板”(Day6 复写时一直在用)
- 先列出共享状态(队列/计数器/stop 标志)
- 再写出 2–3 个 predicate(什么时候能继续、什么时候该退出)
- 所有
wait必须带 predicate - 所有共享状态读写都必须在锁内
- 退出语义必须明确(例如:
stop && queue empty才退出)
二、复写 1:单生产者-单消费者阻塞队列(SPSC BlockingQueue)
设计要点
-
共享资源:
food队列 -
两类等待条件:
- 生产者:桌面没满(
food.size() < max_food) - 消费者:桌面非空(
!food.empty())
- 生产者:桌面没满(
-
wait必须带 predicate,否则会被虚假唤醒坑到 -
生产后唤醒消费者,消费后唤醒生产者(唤醒对象要精准)
✅ 去注释代码(按你版本整理)
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <iostream>
class BlockingQueue {
public:
explicit BlockingQueue(int max_food_) : max_food(max_food_) {}
void Put(int i) {
std::unique_lock<std::mutex> lock(mtx_);
cv_product.wait(lock, [this] { return food.size() < max_food; });
food.push(1);
lock.unlock();
cv_consumer.notify_one();
}
void Take(int i) {
std::unique_lock<std::mutex> lock(mtx_);
cv_consumer.wait(lock, [this] { return !food.empty(); });
food.pop();
lock.unlock();
cv_product.notify_one();
}
private:
int32_t max_food = 10;
std::queue<int> food;
std::mutex mtx_;
std::condition_variable cv_product;
std::condition_variable cv_consumer;
};
BlockingQueue blockingqueue(10);
void Consumers() {
for (int i = 0; i < 100; i++) {
blockingqueue.Take(i);
}
}
void Products() {
for (int i = 0; i < 100; i++) {
blockingqueue.Put(i);
}
}
int main() {
std::thread t1(Consumers);
std::thread t2(Products);
t1.join();
t2.join();
return 0;
}
三、复写 2:写者优先读写锁(Writer-first RWLock)
设计要点(写者优先怎么实现)
我的理解是:写者优先的核心不是“写者抢锁更快”,而是:
只要有写者在等待,就禁止新的读者进入
这样写者不会被源源不断的新读者饿死。
因此读者的进入条件是:
writer_wait == 0 && writer_count == 0
写者的进入条件是:
read_count == 0 && writer_count == 0
释放时的唤醒策略:
- 若还有写者在等:优先唤醒写者
- 否则:放行所有读者
✅ 去注释代码(按你版本整理)
#include <mutex>
#include <condition_variable>
#include <thread>
#include <chrono>
class RWLock {
public:
void WriteUnlock(int i);
void WriteLock(int i);
void ReadUnlock(int i);
void ReadLock(int i);
private:
std::mutex mtx_;
std::condition_variable cv_read;
std::condition_variable cv_writer;
int writer_wait = 0;
int read_count = 0;
int writer_count = 0;
};
int shared_data = 0;
void RWLock::ReadLock(int i) {
std::unique_lock<std::mutex> lock(mtx_);
cv_read.wait(lock, [this] { return writer_wait == 0 && writer_count == 0; });
read_count++;
}
void RWLock::ReadUnlock(int i) {
std::unique_lock<std::mutex> lock(mtx_);
read_count--;
if (writer_wait && read_count == 0) {
lock.unlock();
cv_writer.notify_one();
}
}
void RWLock::WriteLock(int i) {
std::unique_lock<std::mutex> lock(mtx_);
writer_wait++;
cv_writer.wait(lock, [this] { return read_count == 0 && writer_count == 0; });
writer_wait--;
writer_count++;
}
void RWLock::WriteUnlock(int i) {
std::unique_lock<std::mutex> lock(mtx_);
writer_count--;
if (writer_wait) {
lock.unlock();
cv_writer.notify_one();
} else {
lock.unlock();
cv_read.notify_all();
}
}
RWLock rwlock;
void ReadFunc(int i) {
using namespace std::chrono_literals;
rwlock.ReadLock(i);
std::this_thread::sleep_for(500ms);
rwlock.ReadUnlock(i);
}
void WriteFunc(int i) {
using namespace std::chrono_literals;
rwlock.WriteLock(i);
std::this_thread::sleep_for(500ms);
shared_data++;
rwlock.WriteUnlock(i);
}
int main() {
using namespace std::chrono_literals;
std::thread read1(ReadFunc, 1);
std::thread read2(ReadFunc, 2);
std::this_thread::sleep_for(100ms);
std::thread write1(WriteFunc, 1);
std::thread read3(ReadFunc, 3);
read1.join();
read2.join();
write1.join();
read3.join();
return 0;
}
四、复写 3:带返回值线程池(ThreadPool with future)
设计要点
线程池这题最“卡人”的地方有两个:
- worker 的等待条件与退出条件
- 唤醒:
!task_.empty() || stop_ - 退出:
stop_ && task_.empty()
- 任务生命周期(packaged_task 的坑)
packaged_task是 move-only- 不能引用捕获局部变量(否则任务延迟执行时会悬空引用)
- 正确做法是把
packaged_task移动到堆上,用shared_ptr管理生命周期
另外补上一个非常关键的健壮性点:
- stop 后禁止提交(否则 future 可能永远不会 ready)
✅ 去注释代码(按你最终能跑通版本整理)
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <thread>
#include <future>
#include <functional>
#include <memory>
#include <stdexcept>
#include <iostream>
class ThreadPool {
public:
explicit ThreadPool(int i);
~ThreadPool();
void Worker();
std::future<int> Task(std::function<int()> fn);
private:
bool stop_ = false;
std::queue<std::function<void()>> task_;
std::vector<std::thread> workers_;
std::mutex mtx_;
std::condition_variable cv_;
};
ThreadPool::ThreadPool(int i) {
for (int j = 0; j < i; j++) {
workers_.emplace_back(&ThreadPool::Worker, this);
}
}
ThreadPool::~ThreadPool() {
std::unique_lock<std::mutex> lock(mtx_);
stop_ = true;
lock.unlock();
cv_.notify_all();
for (auto& t : workers_) {
t.join();
}
}
void ThreadPool::Worker() {
while (true) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return !task_.empty() || stop_; });
if (task_.empty() && stop_) {
break;
}
std::function<void()> fn = std::move(task_.front());
task_.pop();
lock.unlock();
fn();
}
}
std::future<int> ThreadPool::Task(std::function<int()> fn) {
std::packaged_task<int()> pt(fn);
std::future<int> fut = pt.get_future();
auto int_ptr =
std::make_shared<std::packaged_task<int()>>(std::move(pt));
std::unique_lock<std::mutex> lock(mtx_);
if (stop_) {
throw std::runtime_error("submit on stopped ThreadPool");
}
task_.emplace([int_ptr]() mutable { (*int_ptr)(); });
lock.unlock();
cv_.notify_one();
return fut;
}
int main() {
ThreadPool thread_pool(3);
int f1 = thread_pool.Task([] { return 1; }).get();
int f2 = thread_pool.Task([] { return 2; }).get();
int f3 = thread_pool.Task([] { return 3; }).get();
std::cout << f1 << f2 << f3 << std::endl;
return 0;
}
五、Day6 我踩到的坑与修正
- 引用捕获 packaged_task(悬空引用 / UB)
线程池任务是延迟执行的,必须保证任务对象在执行时仍然存活。
因此必须用move + shared_ptr管理生命周期,而不能用[&]捕获局部变量。 - stop 后仍允许提交的风险
如果 stop 后仍把任务放入队列,有可能造成:worker 全退出、future 永远不 ready、.get()卡死。
所以Task()里必须加stop_检查并拒绝提交。
六、小结:Day6 的收获
Day6 没有引入新 API,但我把“并发题怎么入手”变成了固定套路:
- 先写 predicate
- 明确退出语义
- 保证生命周期正确
- 锁内访问共享状态
- 唤醒对象要精准
到这里,我对互斥锁/条件变量/future 的理解从“能写出来”变成了“能解释为什么这样写”。