手搓 C++ 线程池体检报告:六个组件、四颗雷、一根 C++17 救命稻草
本文章是一份典型的"课程级" C++ 线程池实现:六个组件把 C++11 的多线程基础设施完整手搓了一遍。作为教学代码它是优秀的------生产者-消费者、条件变量谓词、类型擦除、模板方法,一个不少;作为要上生产的代码,它埋着四颗雷,其中三颗我在本文中实测复现了崩溃或 UB。
结论:这份代码能跑,一半靠功底,一半靠 C++17 的强制拷贝省略和 shared_ptr 的类型化析构在底下垫着。
本文做四件事:精读每个组件 用实验实锤雷点 给一个保持原结构的最小侵入修复版 → 再给一个
std::future现代重写版。
目录
- 全景:六个组件,各司其职
- [精读 Any:80 行手搓 std::any](#精读 Any:80 行手搓 std::any)
- [精读 Semaphore:C++20 之前的信号量](#精读 Semaphore:C++20 之前的信号量)
- [精读 Task 与 Result:返回值的接力赛](#精读 Task 与 Result:返回值的接力赛)
- [精读 Thread 与 ThreadPool:教科书级生产者-消费者](#精读 Thread 与 ThreadPool:教科书级生产者-消费者)
- 一次任务的完整生命周期
- 它为什么"碰巧能跑":两个实验
- [代码体检:四颗 P0 雷(三处实测复现)](#代码体检:四颗 P0 雷(三处实测复现))
- 修复版
- [现代版:泛型 submit + future,三件套全删](#现代版:泛型 submit + future,三件套全删)
- 延伸辨析
- 速查总结
1. 全景:六个组件,各司其职
先给每个组件一句话定位,顺便指出它在标准库里的"亲戚"------这是全文最重要的一个观察:
| 组件 | 职责 | 标准库亲戚 |
|---|---|---|
Any |
类型擦除容器,装下 run() 的任意返回类型 |
std::any(C++17) |
Semaphore |
计数信号量,让 get() 阻塞等结果 |
std::counting_semaphore(C++20) |
Task |
任务抽象基类,exec() 是模板方法 |
std::packaged_task / 任意可调用对象 |
Result |
返回值句柄,get() 阻塞取值 |
std::future |
Thread |
线程包装 | std::thread / std::jthread(C++20) |
ThreadPool |
调度核心:队列 + 互斥锁 + 两把条件变量 | ------ |
六个组件里有五个是在手搓 C++17/20 的标准设施。 这不是贬义------手搓一遍恰恰是理解 std::future 那套 shared state 机制的最好方式;但生产代码里,手搓版和标准版的差距就是本文第 8、9 节的内容。
架构图(⚠️ 标记的是后文的雷点):
#mermaid-svg-i8Qz4BXo8ioVZFm8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .error-icon{fill:#552222;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .marker.cross{stroke:#333333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 p{margin:0;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .cluster-label text{fill:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .cluster-label span{color:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .cluster-label span p{background-color:transparent;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .label text,#mermaid-svg-i8Qz4BXo8ioVZFm8 span{fill:#333;color:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .node rect,#mermaid-svg-i8Qz4BXo8ioVZFm8 .node circle,#mermaid-svg-i8Qz4BXo8ioVZFm8 .node ellipse,#mermaid-svg-i8Qz4BXo8ioVZFm8 .node polygon,#mermaid-svg-i8Qz4BXo8ioVZFm8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .rough-node .label text,#mermaid-svg-i8Qz4BXo8ioVZFm8 .node .label text,#mermaid-svg-i8Qz4BXo8ioVZFm8 .image-shape .label,#mermaid-svg-i8Qz4BXo8ioVZFm8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .rough-node .label,#mermaid-svg-i8Qz4BXo8ioVZFm8 .node .label,#mermaid-svg-i8Qz4BXo8ioVZFm8 .image-shape .label,#mermaid-svg-i8Qz4BXo8ioVZFm8 .icon-shape .label{text-align:center;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .node.clickable{cursor:pointer;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .arrowheadPath{fill:#333333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-i8Qz4BXo8ioVZFm8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i8Qz4BXo8ioVZFm8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-i8Qz4BXo8ioVZFm8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .cluster text{fill:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .cluster span{color:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-i8Qz4BXo8ioVZFm8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .icon-shape,#mermaid-svg-i8Qz4BXo8ioVZFm8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .icon-shape p,#mermaid-svg-i8Qz4BXo8ioVZFm8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .icon-shape .label rect,#mermaid-svg-i8Qz4BXo8ioVZFm8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i8Qz4BXo8ioVZFm8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-i8Qz4BXo8ioVZFm8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-i8Qz4BXo8ioVZFm8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工作线程
ThreadPool
用户线程
shared_ptr Task
入队
按值返回 Result ⚠️雷2
取任务
result_ 裸指针 setValue
⚠️雷2
main / 业务代码
Result(栈上对象)
get 阻塞等待
submitTask
notFull 等待 1 秒
taskQue_ 任务队列
mutex + 两把条件变量
threadFunc
notEmpty 等待
Thread::start
detach ⚠️雷1
Task::exec 模板方法
run 用户逻辑
注意最后一条虚线:工作线程通过裸指针直接写用户栈上的 Result 对象------整个设计的核心风险都在这一笔上,后文展开。
2. 精读 Any:80 行手搓 std::any
Any 要解决的问题:Task::run() 的返回类型由用户决定,而线程池框架在编译期不知道它是什么。解法是经典的类型擦除(type erasure):
cpp
class Any
{
// 基类:空壳,只提供虚析构
class Base { public: virtual ~Base() = default; };
// 派生类:模板,把任意类型 T 包进统一的 Base 外壳
template<typename T>
class Derive : public Base { public: Derive(T data):data_(data){} T data_; };
std::unique_ptr<Base> base_; // 基类指针 ------ 类型擦除的关键
public:
template<typename T>
Any(T data) :base_(std::make_unique<Derive<T>>(data)){} // 装
template<typename T>
T cast_()
{
Derive<T>* pd = dynamic_cast<Derive<T>*>(base_.get()); // 取
if (pd == nullptr) { throw "type is unmatch!"; }
return pd->data_;
}
};
#mermaid-svg-esALSsE2hvHw69yR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-esALSsE2hvHw69yR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-esALSsE2hvHw69yR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-esALSsE2hvHw69yR .error-icon{fill:#552222;}#mermaid-svg-esALSsE2hvHw69yR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-esALSsE2hvHw69yR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-esALSsE2hvHw69yR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-esALSsE2hvHw69yR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-esALSsE2hvHw69yR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-esALSsE2hvHw69yR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-esALSsE2hvHw69yR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-esALSsE2hvHw69yR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-esALSsE2hvHw69yR .marker.cross{stroke:#333333;}#mermaid-svg-esALSsE2hvHw69yR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-esALSsE2hvHw69yR p{margin:0;}#mermaid-svg-esALSsE2hvHw69yR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-esALSsE2hvHw69yR .cluster-label text{fill:#333;}#mermaid-svg-esALSsE2hvHw69yR .cluster-label span{color:#333;}#mermaid-svg-esALSsE2hvHw69yR .cluster-label span p{background-color:transparent;}#mermaid-svg-esALSsE2hvHw69yR .label text,#mermaid-svg-esALSsE2hvHw69yR span{fill:#333;color:#333;}#mermaid-svg-esALSsE2hvHw69yR .node rect,#mermaid-svg-esALSsE2hvHw69yR .node circle,#mermaid-svg-esALSsE2hvHw69yR .node ellipse,#mermaid-svg-esALSsE2hvHw69yR .node polygon,#mermaid-svg-esALSsE2hvHw69yR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-esALSsE2hvHw69yR .rough-node .label text,#mermaid-svg-esALSsE2hvHw69yR .node .label text,#mermaid-svg-esALSsE2hvHw69yR .image-shape .label,#mermaid-svg-esALSsE2hvHw69yR .icon-shape .label{text-anchor:middle;}#mermaid-svg-esALSsE2hvHw69yR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-esALSsE2hvHw69yR .rough-node .label,#mermaid-svg-esALSsE2hvHw69yR .node .label,#mermaid-svg-esALSsE2hvHw69yR .image-shape .label,#mermaid-svg-esALSsE2hvHw69yR .icon-shape .label{text-align:center;}#mermaid-svg-esALSsE2hvHw69yR .node.clickable{cursor:pointer;}#mermaid-svg-esALSsE2hvHw69yR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-esALSsE2hvHw69yR .arrowheadPath{fill:#333333;}#mermaid-svg-esALSsE2hvHw69yR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-esALSsE2hvHw69yR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-esALSsE2hvHw69yR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-esALSsE2hvHw69yR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-esALSsE2hvHw69yR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-esALSsE2hvHw69yR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-esALSsE2hvHw69yR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-esALSsE2hvHw69yR .cluster text{fill:#333;}#mermaid-svg-esALSsE2hvHw69yR .cluster span{color:#333;}#mermaid-svg-esALSsE2hvHw69yR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-esALSsE2hvHw69yR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-esALSsE2hvHw69yR rect.text{fill:none;stroke-width:0;}#mermaid-svg-esALSsE2hvHw69yR .icon-shape,#mermaid-svg-esALSsE2hvHw69yR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-esALSsE2hvHw69yR .icon-shape p,#mermaid-svg-esALSsE2hvHw69yR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-esALSsE2hvHw69yR .icon-shape .label rect,#mermaid-svg-esALSsE2hvHw69yR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-esALSsE2hvHw69yR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-esALSsE2hvHw69yR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-esALSsE2hvHw69yR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} unique_ptr<Base>
cast<T> = dynamic_cast<Derive<T>*>
Any
Base(空壳基类)
Derive<int>
data_ = 42
Derive<string>
data_ = hello
它的精髓在于:Any 对外只暴露"能装、能取",类型信息藏在 Derive<T> 里,通过 dynamic_cast 在运行期验证。同一个 Base* 指针,装得下任何 T------这就是类型擦除。
对照 std::any,这 80 行手搓有几个小毛病(都不致命,但值得知道):
| # | 问题 | 说明 |
|---|---|---|
| 1 | Any operator=(const Any&) = delete; |
返回类型写成了 Any 而非 Any&。碰巧它仍是拷贝赋值运算符的重载声明,删除效果没打折扣,属于"错进错出" |
| 2 | 模板构造 Any(T data) 按值收参 |
大对象先拷进参数、再拷进 Derive::data_,两次拷贝;标准库是完美转发 |
| 3 | cast_ 按值返回 pd->data_ |
取出时又拷一次 |
| 4 | throw "type is unmatch!" |
抛的是 const char*,游离于 std::exception 体系之外,catch (const std::exception&) 接不住;std::any_cast 抛的是 std::bad_any_cast |
| 5 | 模板构造没有排除 Any 自身 |
Any b = a;(a 是 Any 左值)会尝试把 Any 包进 Any。本例因拷贝构造被删而侥幸拦截;std::any 用 enable_if 显式约束了这条路 |
另外一个小知识:Any a = "hello"; 会推导 T = const char*(数组按值传参退化成指针),cast_<std::string>() 会失败------std::any 同样如此,这是这一族设计的共性,用的时候注意。
3. 精读 Semaphore:C++20 之前的信号量
C++ 标准库长期没有信号量(直到 C++20 的 std::counting_semaphore),所以手搓一个计数信号量是 C++11/14/17 时代的必修课:
cpp
class Semaphore
{
public:
Semaphore(int limit = 0) : resLimit_(limit) {}
void wait() // 获取一个资源:没有就睡
{
std::unique_lock<std::mutex> lock(mtx_);
cond_.wait(lock, [&]()->bool { return resLimit_ > 0; }); // ✓ 谓词
resLimit_--;
}
void post() // 释放一个资源:叫醒一个等待者
{
std::unique_lock<std::mutex> lock(mtx_);
resLimit_++;
cond_.notify_all();
}
private:
int resLimit_;
std::mutex mtx_;
std::condition_variable cond_;
};
两个细节:
wait用了带谓词的重载 。condition_variable存在虚假唤醒------线程可能在没有任何 notify 的情况下醒来,不用谓词轮询就是 bug。这是多线程入门第一坑,这份代码全部wait都写了谓词,功底在线。resLimit_的读写全部在锁内------不需要原子变量,锁本身就是同步。
一个可改进点:post() 里 notify_all 唤醒所有等待者,但一次 post 只放行一个(resLimit_ 只加了 1),其余线程重测谓词后继续睡------白白付一次惊群的开销。notify_one 就够。C++20 直接换 std::counting_semaphore<>(acquire/release),连锁都不用写。
在本设计中,Semaphore 只有一个用途:让 Result::get() 睡到 setValue() 发生为止------这正是 std::future::get() 阻塞语义的手搓版。
4. 精读 Task 与 Result:返回值的接力赛
Task 和 Result 是一对,共同解决"工作线程怎么把返回值安全交给用户线程":
cpp
class Task {
public:
void exec() // 框架调:模板方法
{
if (result_ != nullptr)
result_->setValue(run()); // 关键三连:跑用户逻辑 → 结果塞给 Result → post 信号量
}
void setResult(Result* res) { result_ = res; } // ⚠️ 裸指针
virtual Any run() = 0; // 用户写:钩子
private:
Result* result_; // ⚠️ 指向用户栈上的对象
};
class Result {
public:
Result(std::shared_ptr<Task> task, bool isvalid = true)
: task_(task), isValid_(isvalid)
{
task->setResult(this); // ⚠️ 把"我"的地址交给 Task
}
Any get()
{
if (!isValid_)
return " "; // ⚠️ 队列满时的"降级":返回一个空格字符串?!
sem_.wait(); // 睡到 setValue
return std::move(any_);
}
void setValue(Any any) { this->any_ = std::move(any); sem_.post(); }
private:
Any any_;
Semaphore sem_;
std::shared_ptr<Task> task_;
std::atomic_bool isValid_;
};
设计上这是个标准的模板方法模式 :exec() 是骨架(搬返回值、发信号),run() 是用户钩子。接力的完整链条是:
worker: task->exec() → run() → result_->setValue(any) → sem_.post()
user: result.get() → sem_.wait() 被唤醒 → 拿到 any_
两个 ⚠️ 是暴雷的种子:
setResult(this)------this是用户栈上 的Result对象的地址,工作线程稍后往这里写。这条指针的生死,就是第 7、8 节的主角。return " "------提交失败时返回一个装着const char*的Any,用户接着cast_<int>()会发生什么?第 8 节实测。
还有一个隐藏问题:Result 的成员 Semaphore sem_ 含 mutex + condition_variable,两者既不可拷贝也不可移动 ,于是 Result 的拷贝/移动构造全部被隐式删除。实测后果:
cpp
std::vector<Result> v;
v.push_back(std::move(result)); // 编译失败
error: call to implicitly-deleted copy constructor of 'Result'
error: static assertion failed due to requirement
'__is_cpp17_move_insertable<std::allocator<Result>, void>::value':
The specified type does not meet the requirements of Cpp17MoveInsertable
原版 Result 连容器都装不进------你没办法攒一批任务批量取结果,API 可用性直接残废。这算 P1 级设计缺陷。
5. 精读 Thread 与 ThreadPool:教科书级生产者-消费者
ThreadPool 的核心是标准的两条件变量生产者-消费者模型:
cpp
// 生产者(用户线程)
Result ThreadPool::submitTask(std::shared_ptr<Task> sp)
{
std::unique_lock<std::mutex> lock(taskQueMtx_);
if (!notFull_.wait_for(lock, std::chrono::seconds(1), // 队列满 → 最多等 1 秒
[&]()->bool { return taskQue_.size() < taskQueMaxThreshHold_; }))
{
std::cerr << "task queue is full,submit fail." << std::endl;
return Result(sp,false); // 超时 → 返回无效 Result
}
taskQue_.emplace(sp);
notEmpty_.notify_all(); // 叫醒消费者
return Result(sp);
}
// 消费者(工作线程)
void ThreadPool::threadFunc()
{
for (;;) // ⚠️ 没有出口
{
std::shared_ptr<Task> task;
{
std::unique_lock<std::mutex> lock(taskQueMtx_);
notEmpty_.wait(lock, [&]()->bool { return taskQue_.size() > 0; }); // ⚠️ 没有停止条件
task = taskQue_.front();
taskQue_.pop();
if (taskQue_.size() > 0) notEmpty_.notify_all(); // 还有活 → 叫醒其他工友
notFull_.notify_all(); // 腾出位置 → 叫醒生产者
} // ← 解锁
if (task != nullptr) task->exec(); // 锁外执行用户任务 ✓
}
}
写得好的地方,点名表扬:
- 锁粒度正确:取任务在锁内,执行任务在锁外。用户任务(可能耗时几秒)绝不占着队列锁。
- 两把条件变量分工明确 :
notFull_给生产者等、notEmpty_给消费者等,比单条件变量 + 广播的写法唤醒精准。 - "队列还有剩就 notify_all" 是多消费者逐个唤醒的经典写法:每次叫醒所有工友,谁抢到谁干,抢不到接着睡。
wait_for(1s)给了队列满一个有限等待 + 明确失败路径,比无限阻塞健壮(虽然失败路径本身是雷 3)。
然后是雷点的种子:
cpp
void Thread::start()
{
std::thread t(func_);
t.detach(); // ⚠️ 雷 1:创建即分离,句柄当场丢弃
}
ThreadPool::~ThreadPool() {} // ⚠️ 雷 1:空析构
threadFunc 的 for(;;) 没有任何出口 ,Thread::start() 把线程一 detach 了之,~ThreadPool() 是空的。三件事凑齐了第 8 节的雷 1。
taskSize_(std::atomic_int)全程在锁内读写,和taskQue_.size()双轨记账,纯冗余------要么用它做无锁快照,要么删掉,现在是白付原子指令的开销。MODE_CACHED是空头支票:setMode只改一个枚举,没有任何动态扩容/回收逻辑。- 超时 1 秒硬编码,不可配置。
6. 一次任务的完整生命周期
把六个组件串成一单生意:
Result(用户栈上) Task 工作线程 任务队列 ThreadPool main(用户线程) Result(用户栈上) Task 工作线程 任务队列 ThreadPool main(用户线程) #mermaid-svg-VDtv7yjWKKlPTDDB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VDtv7yjWKKlPTDDB .error-icon{fill:#552222;}#mermaid-svg-VDtv7yjWKKlPTDDB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VDtv7yjWKKlPTDDB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VDtv7yjWKKlPTDDB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VDtv7yjWKKlPTDDB .marker.cross{stroke:#333333;}#mermaid-svg-VDtv7yjWKKlPTDDB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VDtv7yjWKKlPTDDB p{margin:0;}#mermaid-svg-VDtv7yjWKKlPTDDB .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VDtv7yjWKKlPTDDB text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VDtv7yjWKKlPTDDB .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VDtv7yjWKKlPTDDB .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VDtv7yjWKKlPTDDB #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VDtv7yjWKKlPTDDB .sequenceNumber{fill:white;}#mermaid-svg-VDtv7yjWKKlPTDDB #sequencenumber{fill:#333;}#mermaid-svg-VDtv7yjWKKlPTDDB #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VDtv7yjWKKlPTDDB .messageText{fill:#333;stroke:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VDtv7yjWKKlPTDDB .labelText,#mermaid-svg-VDtv7yjWKKlPTDDB .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .loopText,#mermaid-svg-VDtv7yjWKKlPTDDB .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VDtv7yjWKKlPTDDB .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VDtv7yjWKKlPTDDB .noteText,#mermaid-svg-VDtv7yjWKKlPTDDB .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VDtv7yjWKKlPTDDB .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VDtv7yjWKKlPTDDB .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VDtv7yjWKKlPTDDB .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VDtv7yjWKKlPTDDB .actorPopupMenu{position:absolute;}#mermaid-svg-VDtv7yjWKKlPTDDB .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VDtv7yjWKKlPTDDB .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VDtv7yjWKKlPTDDB .actor-man circle,#mermaid-svg-VDtv7yjWKKlPTDDB line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VDtv7yjWKKlPTDDB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} submitTask(make_shared MyTask(1,100)) notFull 通过,任务入队 notEmpty.notify_all return Result(按值 ⚠️雷2) res.get() → sem.wait() 阻塞 notEmpty 醒来,front + pop task->>exec() run():sum(1..100) result_->>setValue(5050) + sem.post() ⚠️裸指针写入 get() 醒来,cast_<int>() 得 5050
对照 demo 的输出(实测):
$ ./tp_orig
45150
45150
三个 MyTask(1--100 / 101--200 / 201--300)在 4 个工作线程上并行求和,get() 逐个收账,45150 与串行版一致。主流程是通的------问题全在边路:编译期依赖、错误路径、异常路径、关闭路径。这正是"能跑"和"能上线"的区别。
7. 它为什么"碰巧能跑":两个实验
这是我最想讲清楚的一节。原版代码的"正常路径"依赖两个语言机制的兜底,代码本身没有任何防护。
实验 1:降到 C++14,直接编译失败
Result 的成员里,Semaphore(含 mutex + condition_variable)和 std::atomic_bool 都不可拷贝不可移动,Any 的拷贝也被删除------于是 Result 的拷贝构造和移动构造全部被隐式删除。
return Result(sp); 在 C++14 的语义下是"构造临时对象 → 搬进返回槽",这次搬运需要拷贝/移动构造,而它们不存在:
$ clang++ -std=c++14 -fsyntax-only threadpool.cpp
threadpool.cpp:63:10: error: call to implicitly-deleted copy constructor of 'Result'
63 | return Result(sp,false);
| ^~~~~~~~~~~~~~~~
note: copy constructor of 'Result' is implicitly deleted because
field 'any_' has a deleted copy constructor
threadpool.cpp:73:9: error: call to implicitly-deleted copy constructor of 'Result'
73 | return Result(sp);
| ^~~~~~~~~~~~
C++17 起,纯右值(prvalue)"直接在目标内存上构造"(guaranteed copy elision) ------那次搬运在语言层面被取消了,代码才合法。而且正因为取消了搬运,构造函数里的 this 恰好就是调用方 res1 的最终地址,task->setResult(this) 存下的指针才恰好有效。
一句话:
Task::result_那根裸指针能指对地方,全靠 C++17 的强制拷贝省略。 哪天有人把编译标准降回去、或在一个会真正发生移动的返回路径上复用这套设计,立刻翻车。("直接Result r2 = res1;"倒是会被编译器拦住------拷贝/移动都是 deleted,这算 mutex 成员歪打正着立的功。)
实验 2:编译器亲口警告的 Task 析构问题
Task 有虚函数(run()),但析构函数不是 virtual:
cpp
class Task {
public:
Task();
~Task() = default; // ⚠️ 非 virtual
virtual Any run() = 0;
原版在 -Wall -Wextra 下编译,clang 自己跳出来报警(实测原文):
warning: destructor called on non-final 'MyTask' that has virtual
functions but non-virtual destructor [-Wdelete-non-abstract-non-virtual-dtor]
那为什么实际没炸?因为 demo 里任务全部经手 std::make_shared<MyTask>()------shared_ptr 的控制块记住了对象的实际类型,析构走的是类型化 deleter ,精确调用 ~MyTask(),不需要虚析构。又一次"碰巧":只要有人写出 Task* p = new MyTask; delete p;,立刻 UB。
顺带记录:同一编译参数下还有第二个警告,taskQue_.size() < taskQueMaxThreshHold_ 的符号比较(size_t vs int)。
8. 代码体检:四颗 P0 雷(三处实测复现)
雷 1:detach + 空析构 = 线程比池活得久
Thread::start() 里 std::thread t(func_); t.detach(); ------ 线程句柄当场丢弃,从此无人能 join 它。threadFunc 的 for(;;) 没有出口。~ThreadPool() 是空的。
三个条件凑齐后:池析构了,4 个工作线程还活着,还在循环里访问 taskQueMtx_、taskQue_、notEmpty_------一片已经析构的内存。
demo 里 pool 是 main 的栈上对象,return 0 时它销毁,紧接着进程退出,分离线程被顺带带走------通常看起来没事。但这是 UB 的经典形态:线程要么阻塞在已销毁的条件变量上等死,要么在池销毁到进程退出之间的窗口里醒来摸一把废墟。把池放进任何比 main 短的生命周期(函数返回、服务热重载、单测里每个 case 建一个池),轻则 hang 死、重则内存损坏。
正确姿势只有一种:停止标志 + notify_all + join (见修复版)。detach 只在线程不接触任何可能先于它死亡的数据时才安全------工作线程永远不满足这个前提。
雷 2:Task 裸指针 + Result 按值返回 = 生命周期走钢丝
第 7 节已经展示了"编译期"的那一半(C++14 直接失败、C++17 靠省续命)。真正无防护的是运行期场景:
cpp
pool.submitTask(std::make_shared<QuickTask>()); // 返回的 Result 被丢弃
临时 Result 在这条语句结束时析构;但任务还在队列里排队。等轮到它执行时,task->exec() 会通过 result_ 往那块已析构的栈内存 里 setValue + sem_.post()------锁一把已销毁的 mutex。
实测复现(单线程池 + 慢任务占坑 + 丢弃 Result,完整代码见 ub_demo.cpp):
$ clang++ -std=c++17 -fsanitize=address ... ub_demo.cpp
$ ./tp_ub
libc++abi: terminating due to uncaught exception of type
std::__1::system_error: mutex lock failed: Invalid argument
Abort trap: 6(exit code 134)
工作线程对已析构 Semaphore 的 mutex 上锁,libc++ 检测出无效句柄直接抛异常,线程内无人接 → terminate。这还是"运气好炸得响"的形态;运气不好就是 any_ 静默写坏、数据悄悄损坏。
雷 3:队列满 → 错误路径必崩
顺一遍这条"优雅降级"链:
- 队列满 1 秒 →
submitTask返回isValid_ = false的Result - 用户按正常业务代码写
int v = res.get().cast_<int>(); get()里if (!isValid_) return " ";------返回一个装着const char*的Anycast_<int>():dynamic_cast<Derive<int>*>失败 →throw "type is unmatch!"(const char*)- 没人接 →
std::terminate
实测复现(队列容量 1、2 线程、慢任务,完整代码见 crash_demo.cpp):
$ ./tp_crash
task queue is full,submit fail. ← submitTask 的 cerr
r3 被 rejected,下面调用 r3.get() ... ← 之后:
libc++abi: terminating due to uncaught exception of type char const*
Abort trap: 6(exit code 134)
提交失败是正常业务必须处理的分支(限流、降级、重试),而这条路径在本设计中 100% 崩溃。 修法很简单:失败就抛 std::runtime_error,或者给 get() 一个显式的 bool try_get(Any&),但绝不能返回一个假 Any 让用户 cast 崩。
雷 4:run() 异常逃逸 + 信号量悬挂
用户任务的 run() 一旦抛异常:
- 异常从
run()冲进exec()------exec没有 try/catch------冲进threadFunc的for(;;)------也没有------冲出线程入口函数 。分离线程里的未捕获异常 →std::terminate,全进程死。 - 就算侥幸不死:
setValue没执行 →sem_.post()永远不会被调用 → 所有在这任务上get()的用户线程永久阻塞。
双杀。正确姿势:exec() 里 try/catch,用 std::exception_ptr 把异常存进共享状态,post() 放在 catch 之后无条件执行;用户在 get() 处接住重新抛出的异常(这正是 std::future::get() 的行为)。
P1 / P2 清单
| 级别 | 问题 | 位置 |
|---|---|---|
| P1 | taskSize_ 原子计数冗余(全程锁内,与 queue.size() 双轨记账) |
ThreadPool |
| P1 | Any 拷贝赋值返回类型写错(Any 非 Any&) |
Any |
| P1 | 模板构造按值收参(两次拷贝)、cast_ 拷贝返回 |
Any |
| P1 | throw 字符串字面量,游离于 std::exception 体系外 |
Any |
| P1 | Task 析构非 virtual(编译器已警告,靠 shared_ptr 侥幸) |
Task |
| P1 | MODE_CACHED 空头支票,无扩容/回收逻辑 |
ThreadPool |
| P1 | Result 不可拷贝/移动,装不进容器(实测 static_assert 失败) |
Result |
| P1 | 超时 1 秒硬编码 | ThreadPool |
| P2 | ThreshHold 拼写(应 Threshold);"上线"应为"上限";"丛"应为"从" |
注释/命名 |
| P2 | cpp 用了 std::chrono 却没 include(libc++ 的 <condition_variable> 间接引入,标准不保证) |
threadpool.cpp |
| P2 | post() 持锁 notify(合法但低效);库代码直接 cerr |
Semaphore/ThreadPool |
| P2 | Thread 类无 id、无停止概念,纯"函数壳" |
Thread |
9. 修复版:四刀手术
修复版的原则:保持课程版的结构和 API 不变(仍然 Task/Result/submitTask 那一套),只做最小侵入的修复,让每个改动都能明确对应一颗雷。
| 雷 | 手术 | 刀口 |
|---|---|---|
| 雷 1 detach/空析构 | Thread 持有 std::thread 成员;析构 = 停止标志 + notify_all + 逐个 join;threadFunc 增加"队列排空且已停止 → return"(drain 策略) |
Thread / ThreadPool |
| 雷 2 裸指针回指栈对象 | 引入堆上共享状态 ResultState :Task 写、Result 读、shared_ptr 共管生命周期;Result 变成轻量句柄,可拷贝可移动 |
ResultState |
| 雷 3 错误路径崩溃 | 提交失败 → isValid_ 保持 false → get() 第一时间 throw std::runtime_error,绝不返回假 Any |
Result::get |
| 雷 4 异常逃逸 | exec() 捕获一切异常存入 exception_ptr;post() 无条件执行;get() 处 rethrow |
Task::exec |
其中第二刀是心智模型的升级 ,值得多说一句:Result 不再是"一个装着信号量的栈上对象",而是"指向堆上共享状态的轻量句柄"------Task 往信箱里投递,Result 从信箱里取信,shared_ptr 保证信箱比双方都活得久。这正是 std::future / std::packaged_task 的 shared state 模式 。你在课程里手搓的 Result + Task + Semaphore 三件套,合起来就是 future 体系的雏形。
修复版完整代码(头文件):
cpp
#ifndef THREADPOOL_H
#define THREADPOOL_H
// =====================================================================
// 修复版线程池(保持课程版的结构与 API,修掉四颗 P0 雷 + 一批 P1/P2)
// [修1] detach + 空析构 → RAII 线程 + 停止标志 + drain + join
// [修2] Task 裸指针回指栈对象 → ResultState 共享状态(shared_ptr 共管)
// [修3] 队列满 get() 返回 " " → get() 抛 std::runtime_error
// [修4] run() 异常逃逸线程 → exception_ptr 存档 + 无条件 post
// =====================================================================
#include <vector>
#include <queue>
#include <memory>
#include <atomic>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <functional>
#include <chrono>
#include <stdexcept>
#include <exception>
#include <type_traits>
#include <utility>
// ============ Any:类型擦除容器(修正版) ============
class Any
{
public:
Any() = default;
~Any() = default;
Any(const Any&) = delete;
Any& operator=(const Any&) = delete; // [修] 原版返回类型误写成 Any
Any(Any&&) = default;
Any& operator=(Any&&) = default;
// [修] 完美转发 + decay,避免大对象两次拷贝;
// enable_if 排除 Any 自身,防止把 Any 包进 Any(std::any 同款约束)
template <typename T,
typename = std::enable_if_t<!std::is_same_v<std::decay_t<T>, Any>>>
Any(T&& data)
: base_(std::make_unique<Derive<std::decay_t<T>>>(std::forward<T>(data))) {}
// [修] 抛标准异常(原版 throw 字符串字面量);移动取出(原版按值拷贝返回)
template <typename T>
T cast()
{
auto* pd = dynamic_cast<Derive<T>*>(base_.get());
if (pd == nullptr)
throw std::runtime_error("Any::cast: type mismatch");
return std::move(pd->data_);
}
private:
struct Base { virtual ~Base() = default; };
template <typename T>
struct Derive : Base
{
explicit Derive(T d) : data_(std::move(d)) {}
T data_;
};
std::unique_ptr<Base> base_;
};
// ============ Semaphore:计数信号量 ============
class Semaphore
{
public:
explicit Semaphore(int limit = 0) : resLimit_(limit) {}
void wait()
{
std::unique_lock<std::mutex> lock(mtx_);
cond_.wait(lock, [&] { return resLimit_ > 0; });
--resLimit_;
}
void post()
{
{
std::lock_guard<std::mutex> lock(mtx_);
++resLimit_;
}
cond_.notify_one(); // [修] 一次 post 只放行一个 waiter,notify_one 足够
}
private:
int resLimit_;
std::mutex mtx_;
std::condition_variable cond_;
};
// ============ 共享状态:Task 生产、Result 消费 ============
// [修2] 原版 Task 用裸指针 Result* 回指用户栈上的对象,生命周期全靠运气;
// 现在双方各持 shared_ptr,状态活在堆上 ------ 这正是 std::future 的 shared state 模式
struct ResultState
{
Any any_; // 任务的返回值
Semaphore sem_; // get() 与 exec() 之间的同步
bool isValid_ = false; // 提交是否被池接受
std::exception_ptr eptr_ = {}; // [修4] run() 抛出的异常存档
};
// ============ Result:返回值句柄(可拷贝、可移动、可进容器) ============
class Result
{
public:
Result() = default;
explicit Result(std::shared_ptr<ResultState> state) : state_(std::move(state)) {}
Any get()
{
if (!state_ || !state_->isValid_)
throw std::runtime_error(
"Result::get: task was not accepted (queue full / timeout)"); // [修3]
state_->sem_.wait();
if (state_->eptr_)
std::rethrow_exception(state_->eptr_); // [修4]
return std::move(state_->any_);
}
private:
std::shared_ptr<ResultState> state_; // [修2] 句柄化:Semaphore 不再是成员
};
// ============ Task:任务抽象基类 ============
class Task
{
public:
virtual ~Task() = default; // [修] 原版析构非 virtual
void attachState(std::shared_ptr<ResultState> state) { state_ = std::move(state); }
// 模板方法:框架负责搬返回值、传异常、发信号;子类只管写 run()
void exec()
{
if (!state_)
return;
try
{
state_->any_ = run();
}
catch (...)
{
state_->eptr_ = std::current_exception(); // [修4] 异常存档,不逃逸线程
}
state_->sem_.post(); // [修4] 无论成败必发信号,get() 永不悬挂
}
virtual Any run() = 0;
private:
std::shared_ptr<ResultState> state_; // [修2] 原版:Result* result_ 裸指针
};
// ============ 线程池支持的模式 ============
enum class PoolMode
{
MODE_FIXED, // 固定线程数
MODE_CACHED, // 动态扩容(原版即未实现,本版保持现状,见 §11)
};
// ============ Thread:RAII 线程包装 ============
class Thread
{
public:
using ThreadFunc = std::function<void()>;
explicit Thread(ThreadFunc func) : func_(std::move(func)) {}
~Thread()
{
if (thread_.joinable())
thread_.join(); // [修1] 兜底:joinable 的 std::thread 析构会 terminate
}
void start() { thread_ = std::thread(func_); }
void join() { if (thread_.joinable()) thread_.join(); }
private:
ThreadFunc func_;
std::thread thread_; // [修1] 原版:局部 std::thread 直接 detach 后丢弃句柄
};
// ============ ThreadPool ============
class ThreadPool
{
public:
ThreadPool() = default;
~ThreadPool(); // [修1] 优雅关闭:停止标志 + 唤醒 + join
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
void setTaskQueMaxThreshold(size_t threshold) { taskQueMaxThreshHold_ = threshold; }
void setMode(PoolMode mode) { poolMode_ = mode; }
Result submitTask(std::shared_ptr<Task> sp);
void start(size_t initThreadSize = 4);
private:
void threadFunc();
std::vector<std::unique_ptr<Thread>> threads_;
size_t initThreadSize_ = 0;
std::queue<std::shared_ptr<Task>> taskQue_;
size_t taskQueMaxThreshHold_ = 4; // [修] 干掉冗余的 taskSize_,队列长度即真相
std::mutex taskQueMtx_;
std::condition_variable notFull_;
std::condition_variable notEmpty_;
std::atomic_bool isRunning_{ false }; // [修1] 停止标志
PoolMode poolMode_ = PoolMode::MODE_FIXED;
};
#endif // THREADPOOL_H
实现文件(只有 ThreadPool 需要出类定义,其余都在头文件内联):
cpp
#include"threadpool.h"
#include<iostream>
// [修1] 线程池析构:优雅关闭 ------ 置停止标志、唤醒全部线程、排空队列后逐个 join
ThreadPool::~ThreadPool()
{
{
std::lock_guard<std::mutex> lock(taskQueMtx_);
isRunning_ = false;
}
notEmpty_.notify_all();
for (auto& t : threads_)
if (t) t->join();
}
void ThreadPool::start(size_t initThreadSize)
{
initThreadSize_ = initThreadSize;
isRunning_ = true;
for (size_t i = 0; i < initThreadSize_; ++i)
{
threads_.emplace_back(
std::make_unique<Thread>([this] { threadFunc(); })); // [修] lambda 代替 bind
}
for (auto& t : threads_)
t->start();
}
Result ThreadPool::submitTask(std::shared_ptr<Task> sp)
{
// [修2] 先在堆上创建共享状态,Task 与 Result 各持一份 shared_ptr
auto state = std::make_shared<ResultState>();
sp->attachState(state);
std::unique_lock<std::mutex> lock(taskQueMtx_);
if (!notFull_.wait_for(lock, std::chrono::seconds(1),
[&] { return taskQue_.size() < taskQueMaxThreshHold_; }))
{
std::cerr << "[ThreadPool] task queue full, submit rejected" << std::endl;
// [修3] isValid_ 保持 false,get() 会抛出明确异常,而不是返回 " "
return Result(std::move(state));
}
state->isValid_ = true;
taskQue_.emplace(std::move(sp));
notEmpty_.notify_all();
return Result(std::move(state));
}
void ThreadPool::threadFunc()
{
for (;;)
{
std::shared_ptr<Task> task;
{
std::unique_lock<std::mutex> lock(taskQueMtx_);
// [修1] 谓词加入停止条件;队列排空 + 池已停止 → 有序退出(drain 策略)
notEmpty_.wait(lock, [&] { return !taskQue_.empty() || !isRunning_; });
if (taskQue_.empty() && !isRunning_)
return;
task = taskQue_.front();
taskQue_.pop();
//通知其他线程一起来消费
if (!taskQue_.empty())
notEmpty_.notify_all();
//通知生产者生产任务
notFull_.notify_all();
}//释放锁,锁粒度与原版一致(这点原版写对了)
if (task != nullptr)
{
task->exec();
}
}
}
四个场景的验证程序覆盖四颗雷的"拆雷"效果,实测输出:
$ clang++ -std=c++17 -Wall -Wextra -pthread threadpool.cpp main.cpp && ./tp_fixed
场景1 sum(1..300) = 45150
场景1 pool 已安全析构(drain + join 完成,无 detach)
场景2 捕获任务异常: boom: task failed
[ThreadPool] task queue full, submit rejected ← stderr
场景3 捕获提交失败: Result::get: task was not accepted (queue full / timeout)
全部场景通过,进程正常退出
exit=0(总耗时 ~4.3s:场景 3 的 drain 把两个慢任务排空后才退出)
对照原版在同样场景下的表现:场景 2 → terminate(雷 4);场景 3 → terminate(雷 3);场景 1 结束时 → detach 线程摸废墟(雷 1,赌运气)。修复版零警告编译,四个场景全部优雅处理,进程干净退出。
另外注意场景 1 里 std::vector<Result> 现在能用了------句柄化之后 Result 可拷贝可移动,攒一批任务批量收账的需求终于得到支持。
10. 现代版:泛型 submit + future,三件套全删
修复版修好了雷,但仍保留着手搓的 Any / Semaphore / Result / Task 四件套。现代版换个问法:如果目标是 C++17 起步,这些还应该手搓吗?
cpp
#ifndef THREADPOOL_MODERN_H
#define THREADPOOL_MODERN_H
// =====================================================================
// 现代版线程池:泛型 submit + std::future
// 原版手搓的五个类里,Any / Semaphore / Result / Task / Thread 全部删除
// =====================================================================
#include <condition_variable>
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <type_traits>
#include <vector>
class ThreadPool {
public:
explicit ThreadPool(std::size_t threads = std::thread::hardware_concurrency())
{
if (threads == 0)
threads = 1;
for (std::size_t i = 0; i < threads; ++i)
{
workers_.emplace_back([this]
{
for (;;)
{
std::function<void()> job;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return stopped_ || !jobs_.empty(); });
if (stopped_ && jobs_.empty())
return; // drain 策略:停止前排空队列
job = std::move(jobs_.front());
jobs_.pop();
}
job(); // packaged_task 在这里触发,返回值/异常自动进 future
}
});
}
}
~ThreadPool()
{
{
std::lock_guard<std::mutex> lock(mtx_);
stopped_ = true;
}
cv_.notify_all();
for (auto& w : workers_)
if (w.joinable()) w.join();
}
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
// 提交任意可调用对象:返回值类型自动推导,强类型 get(),异常自动透传
template <class F, class... Args>
auto submit(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>>
{
using R = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<R()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<R> fut = task->get_future();
{
std::lock_guard<std::mutex> lock(mtx_);
if (stopped_)
throw std::runtime_error("submit on stopped ThreadPool");
jobs_.emplace([task] { (*task)(); });
}
cv_.notify_one();
return fut;
}
private:
std::vector<std::thread> workers_;
std::queue<std::function<void()>> jobs_;
std::mutex mtx_;
std::condition_variable cv_;
bool stopped_ = false;
};
#endif // THREADPOOL_MODERN_H
使用体验的变化是质变级的:
cpp
ThreadPool pool(4);
// 不再继承 Task、不再写子类:lambda + 参数直接上,返回类型自动推导
auto fsum = pool.submit([](int b, int e) {
int s = 0; for (int i = b; i <= e; ++i) s += i; return s;
}, 1, 100); // std::future<int>
auto fstr = pool.submit([] { return std::string("hello from worker"); });
// std::future<std::string>
int x = fsum.get(); // 强类型,没有 cast_<int>(),没有运行期类型检查
std::string s = fstr.get(); // 装错类型?编译期就告诉你
实测输出(零警告编译):
$ clang++ -std=c++17 -Wall -Wextra -pthread main.cpp && ./tp_modern
sum(1..100) = 5050
string = hello from worker
caught: boom
modern pool 析构:drain + join 完成,正常退出
exit=0
caught: boom 那行值得停一下看:任务里的异常被 packaged_task 捕获存进 future,在 fut.get() 处重新抛出,被用户线程的 catch 接住------雷 4 的正确形态,标准库内置。四颗雷在这版里的归宿:
| 原版雷点 | 现代版的归宿 |
|---|---|
| 雷 1 detach/空析构 | 析构 = 停止 + join,与修复版同款逻辑 |
| 雷 2 裸指针/生命周期 | shared state 在堆上由 future/packaged_task 共管,用户侧 future 只是句柄 |
| 雷 3 队列满崩溃 | submit 后立刻拿到 future;池关闭后再 submit 抛 runtime_error(超时策略可按需加回) |
| 雷 4 异常逃逸 | packaged_task 内置捕获,get() 处 rethrow |
各组件的标准库对照表(本文的"中心思想"):
| 手搓组件 | 标准库替代 | 获得什么 |
|---|---|---|
Any(80 行) |
std::any + 模板推导 |
std::bad_any_cast、无手写 dynamic_cast;而 submit 泛型化后根本不需要运行期类型擦除 |
Semaphore(35 行) |
future/promise 的共享状态语义;C++20 std::counting_semaphore |
不再手搓锁 + 谓词 |
Result(40 行) |
std::future<T> |
可移动句柄、异常透传、wait_for 超时,全内置 |
Task 子类 + run() |
std::packaged_task / 任意可调用对象 |
不用继承,lambda 直接上 |
Thread 包装(20 行) |
std::thread;C++20 std::jthread |
jthread 析构自动 stop + join |
代码量:原版 ~330 行 → 现代版 ~90 行。少掉的 240 行,就是第 8 节那四颗雷的全部藏身处。
一个遗留的小限制:submit 里用 std::bind 转发参数,move-only 参数(如 unique_ptr)会卡在 bind 的拷贝语义上------需要的话改用 lambda + std::tuple + std::apply 的惯用法,本文不展开。
11. 延伸辨析
线程数怎么定
- CPU 密集型 :线程数 ≈ 核数(
std::thread::hardware_concurrency(),注意可能返回 0,要兜底)。多了只有上下文切换的损耗。 - IO 密集型:线程数 ≈ N × (1 + W/C),W/C 是等待时间与计算时间之比;经验值从 2N 起步试。
MODE_CACHED的正确实现思路(原版未实现):任务入队时记时间戳,消费者发现任务等待超阈值且线程数未达上限 → 扩线程;线程空闲超过 60s 且线程数高于初始值 → 回收(线程自然退出)。适用场景:突发大量短 IO 任务。固定池 + 足够大的队列 + 上游限流,通常比动态池简单且够用。
为什么不直接用 std::async 当线程池
launch::async:标准不保证线程复用,可能每个任务新建线程------没有"池"。launch::deferred:get()时才在调用线程惰性执行------根本没有多线程。- 默认策略
async | deferred:实现自选,行为不可预期。 - 没有队列上限、没有背压、没有优雅关闭。结论:async 适合"顺手并行一个孤立任务",不适合做资源池。
notify_one 还是 notify_all
- 一次
post只放行一个等待者 →notify_one语义正确且省惊群。 - 原版
threadFunc里"队列还有剩就 notify_all"是多消费者逐个唤醒 的经典写法,属于正确用法;但Semaphore::post里的notify_all是浪费(N-1 个线程白醒一轮,重测谓词继续睡)。 - 经验法则:一次事件只允许一个线程消费 → one;状态变化对所有等待者有意义(如"队列非空")→ all 或循环 one。
关闭策略:drain 还是 abandon
修复版/现代版都选了 drain(排空再走) :已提交的任务保证执行完------适合任务不能丢的场景(日志落盘、消息发出去了)。代价是析构要等队列清空。反过来 abandon(立刻走) :析构快,但排队任务作废------适合任务可重试/幂等/上游会重发的场景。改造很简单:谓词直接判 !isRunning_ 就退出,丢弃队列剩余任务。关键是两种都要"唤醒 + join",detach 哪种都不行。
C++20 的两个升级点
cpp
// 1) std::jthread:析构自动 request_stop() + join(),雷 1 的官方解法
std::vector<std::jthread> workers_;
workers_.emplace_back([this](std::stop_token st) {
while (!st.stop_requested()) {
// ... 等任务、执行
}
});
// 2) std::counting_semaphore:Semaphore 的官方替代
std::counting_semaphore<> sem{0};
sem.acquire(); // wait
sem.release(); // post
12. 速查总结
| 组件 | 原版的雷 | 修复版刀法 | 标准库替代 |
|---|---|---|---|
| Thread | detach + 空析构 = 析构后线程摸废墟(UB) | 持有 std::thread + 停止标志 + join |
std::jthread(C++20) |
| Task/Result | 裸指针回指栈上 Result,靠 C++17 省略续命;Result 不可移动 | 堆上 ResultState 共享状态,shared_ptr 共管 |
future 的 shared state |
| 错误路径 | 队列满 → return " " → cast_ 抛 const char* → terminate(实测) |
get() 抛 std::runtime_error |
future 异常体系 |
| 异常路径 | run() 抛异常 → 逃逸线程 → terminate + 信号量悬挂(双杀) |
exception_ptr 存档 + 无条件 post() + get() rethrow |
packaged_task 内置 |
| Any | 签名瑕疵、双重拷贝、抛字符串字面量 | 完美转发 + enable_if 约束 + 标准异常 |
std::any / 泛型推导 |
| 其他 | taskSize_ 冗余、Task 析构非 virtual、MODE_CACHED 空头支票 |
见 P1/P2 清单 | ------ |
面试应答版(这份代码恰好是"线程池"面试题的标准追问清单):
- 怎么优雅关闭线程池? 停止标志(锁内写)→
notify_all唤醒所有阻塞在notEmpty上的线程 → 谓词加入!running→ 队列排空后 return → 主线程 join。绝不能 detach。 - 任务返回值怎么安全传回? 堆上共享状态 +
shared_ptr双方共管(future 模式),绝不让工作线程持有指向用户栈的裸指针。 - 任务里抛异常怎么办?
catch(...)+std::current_exception()存档,get()处rethrow_exception;且必须保证"发信号"在 catch 之后无条件执行,否则get()永久悬挂。 - 为什么
wait必须带谓词? 虚假唤醒是condition_variable的规范行为,不带谓词的wait是 bug。 - detach 什么时候安全? 仅当线程不接触任何可能先于它死亡的数据。线程池工作线程引用池的成员,而池一定先死------所以永远不安全。
最后回到那份原代码:它作为教学样本完成度很高------六个组件的骨架 全部正确(锁粒度、谓词、两条件变量、模板方法),错的全是生命周期与错误路径 这类"跑通了就看不见"的暗礁。而这两个方向,恰好就是 C++ 并发面试和生产事故的高发区。手搓一遍,再用 std::future 重写一遍,两头都吃透------这份代码的教学价值就榨干了。