高级线程管理:线程池
读完能用一个固定工作线程队 + 线程安全任务队列实现最简线程池,能用 packaged_task 让 submit 返回 future 取回任务结果(或异常),能解释工作窃取(每线程一条本地双端队列、空了去同事队尾偷)如何在减少争用的同时均衡负载,并知道线程为什么要「协作式中断」(置标志 + 中断点自查)而不是强杀。
学习目标
- 能实现具备接受、排空、拒绝新提交和 join 协议的 C++17 线程池,并让每个已接受任务的 future 以值或异常结束
- 能分析池内任务依赖导致的耗尽死锁,并比较全局队列、本地队列与工作窃取的争用和局部性
- 能回答:为什么要给线程做「协作式中断」(置标志 + 线程到中断点自查),而不是直接「强杀」一个正在跑的线程?强杀会出什么问题?
机制总览
线程池提交、调度与停止协议
- 1
提交任务
packaged_task 把 callable 与 future 结果绑定,关闭后拒绝新任务。
- 2
调度执行
worker 优先本地队列,空闲时窃取其他队列尾部以平衡负载。
- 3
停止线程池
停止信号、排空策略、任务取消和 join 顺序构成单一协议。
章级决策实验
线程池提交、调度与停止协议
沿任务生命周期检查队列争用、future 结果、工作窃取和协作停止。
选择推理阶段
当前阶段 · 提交任务
packaged_task 把 callable 与 future 结果绑定,关闭后拒绝新任务。
可核验证据
提交状态、队列结果与 broken promise 测试。
线程池是任务所有权系统;提交失败、任务异常和关闭时未完成工作都必须有明确结果。
失效—证据矩阵
线程池提交、调度与停止协议
提交任务
典型失效
任务入队失败却返回永不就绪的 future。
核验证据
提交状态、队列结果与 broken promise 测试。
调度执行
典型失效
所有线程争用一个全局队列,或池内互等 future 造成饥饿。
核验证据
队列长度、窃取次数与阻塞栈。
停止线程池
典型失效
析构时仍接收任务,或强杀正在持锁的 worker。
核验证据
shutdown trace、未完成任务数与重复停止测试。
从“任务被接受后谁负责到底”开始
前几章你学会了喊一个帮厨(起一个线程)、给他派活、等他把菜端回来。可真到了饭点,订单一张接一张涌进来——你总不能每来一道菜就现雇一个厨师,做完一道就把他辞退吧?招人、培训、结算、送走,这套手续本身就比炒一盘菜还慢。
更聪明的做法是:先雇一小队固定的帮厨待命。来了活儿就丢进一个公用的「单子筐」,哪个帮厨手头空了就过去抓一张单子做,做完回来再抓下一张。订单再多,也就这几个人轮着干——省下了反复雇人、辞人的全部折腾。
没有这套机制会怎样?要么你为每道菜新雇/辞退一个厨师,光在「招人送人」上就把时间耗光(频繁建销线程的开销);要么所有人都挤在同一个单子筐前抢单子(争用),或者有人累死、有人闲死(负载不均)。这一章就教你把这支「常备帮厨队」搭起来、调顺。
为什么要线程池:固定一队工人,循环复用
↡长期持有一组工作线程,由调度队列把多个任务映射到这些线程。线程预算由 CPU、阻塞比例、任务粒度和共存负载共同决定;池还必须定义提交、关闭、取消和结果完成协议。 复用一组 ↡线程池长期持有并反复执行任务的线程。空闲时应阻塞等待;退出前要完成或取消已接受任务,并由池所有者 join。。 任务进入 ↡保存已接受但尚未开始执行任务的调度结构。它必须线程安全、有明确容量与关闭语义,并保证排空或取消时每个关联结果都能终结。, 由空闲 worker 取走;线程数不是直接等于 CPU 核数,而是从硬件提示和负载测量得到预算。
为什么值得这么做?因为创建和销毁线程都不便宜——要向操作系统申请栈空间、注册内核线程、调度上下文……单看一次不起眼,可在「每个任务一个线程」的高频场景里,这笔开销会盖过任务本身。线程池把固定那几个线程复用起来:任务来了只是排进队列,由现成的空闲工人取走,谁也不用新建。下面这张主 Demo 把「提交任务 → 入队 → 空闲工人取走执行 → 做完再取」一拍一拍演给你看:
猜一猜:来一个任务就新建一个线程、做完就销毁,和「固定三个工人循环取任务」,在订单暴涨时哪个更快?为什么固定那几个工人不会「不够用」也不会「太浪费」?先想一下,再单步看这张图。
第 1 / 5 步 · 提交任务:T1~T4 全进任务队列(单子筐)排队等做
固定三个工人 worker0/1/2 循环「取任务→执行→再取」:任务进队列排队,谁空了抓一张做,做完接着抓下一张——线程被复用,省去每来一个任务就新建/销毁线程的开销。可暂停、单步、拖进度。
看清楚了:四个任务 T1~T4 进队列排队,三个固定工人 worker0/1/2 谁空了就抓一张做,worker0 做完 T1 后还是它自己接着抓 T3——不新建第四个线程。这就是「复用」:无论来多少任务,活跃线程数始终是那固定几个。
线程池不能只定义“怎么跑”,还要定义“怎么停”。本章采用 drain 语义:关闭开始后拒绝新 submit,已入队任务继续执行;队列排空后 worker 退出,所有线程 join 后对象才可销毁。另一种 cancel-pending 语义也可以,但必须让未执行任务的 future 得到明确取消异常,不能永久等待。
等待任务结果:submit 返回一张 future
光把任务丢进队列还不够——你常常想知道任务算出了什么。这里正好接上第四章学过的 std::future 和 packaged_task:submit 一个任务时,用 packaged_task 把它包起来、关联一张 future,立刻把这张 future「取餐凭证」还给你;任务被某个工人执行完,结果(或抛出的异常)就被填进这张凭证,你 get() 一下就拿到。
packaged_task 在这里是关键拼图:它可移动,所以能被搬进任务队列、由别的工作线程取出来调用——这正是第四章埋的伏笔。代码一节会把 submit 的写法完整拆开。
池内任务依赖:等待时也要保留进展
任务依赖最危险的形态,是池内父任务提交子任务后阻塞 future.get()。若所有 worker 都这样等待,子任务虽然已在队列里,却没有空闲线程执行,形成 pool exhaustion deadlock。增加线程数只能推迟耗尽,不能证明安全。
C++17 的标准 future 没有 .then。可用三种协议表达依赖:把依赖图调度成子任务完成后再提交 continuation;等待期间由当前 worker 执行 run_pending_task() 帮助队列取得进展;或把可能阻塞的任务隔离到另一执行资源。每种方案都要处理异常与取消,确保下游任务不会永远等一个已失败的前置节点。
减少争用 + 工作窃取:每人一条本地队列
最简线程池所有工人抢同一个全局任务队列——任务一多,大家都卡在那把队列锁上排队(争用)。更进阶的做法是给每个工作线程配自己的一条 ↡double-ended queue 的简称,一种两端都能 push/pop 的队列。工作窃取里每个工作线程持有一条本地 deque:自己从「本端」push/pop(互不干扰),别的线程来偷时从「另一端(队尾)」拿——两端分开操作,把争用降到最低。:本线程提交的任务压进自己这条队列、也优先从自己这条取,互不打扰,争用大降。
可这样一来,有人忙得队列堆满、有人早早干完闲着,岂不浪费?于是再加一招 ↡一种线程池负载均衡策略:每个工作线程有自己的本地双端队列,平时只动自己的队列(减少争用);一旦自己队列空了,就去别的忙碌线程的队列「另一端(队尾)」偷一个任务来做。这样既保留本地队列的低争用,又让空闲线程不白等、自动均衡负载。:哪个工人自己的队列空了,就去**别人队列的另一端(队尾)**偷一个任务来做。下面这张动画把「各取本端 → 有人空了 → 去同事队尾偷 → 全队不闲」演一遍:
第 1 / 4 步 · 各 worker 从自己队列的「本端(front)」取活做(LIFO)——本地取放,互不争用
每个 worker 有自己的本地双端队列:平时从「本端」取放任务(互不争用一个全局队列);自己空了就去别人队列的「另一端(尾)」偷一张干——本地队列减争用、工作窃取均衡负载。可暂停、单步、拖进度。
盯住两个细节:① 平时各取自己队列的本端(互不争用一个全局队列);② 偷的时候从对方队尾拿,而不是和对方抢本端——两端分开,连偷取都尽量不冲突。本地队列减争用、工作窃取均衡负载,两招合起来就是高性能线程池的核心。
协作式中断:发信号,线程到安全点自己退
最后一块拼图:怎么停一个还在跑的工作线程?比如池要销毁了、或某个任务该取消了。最粗暴的念头是「强杀」——可线程被强杀时可能正攥着一把锁、正写到一半某块数据,当场掐断会留下锁不释放、数据破损的烂摊子。所以 C++ 不提供强杀线程的手段。
正确做法是 ↡停止一个线程的安全方式:不强制杀死它,而是给它「置一个中断标志」当作信号;目标线程在自己代码里的若干「中断点」主动检查这个标志——没置位就继续干,置位了就主动抛出异常 / 退出循环、做完清理再走。停不停、何时停由目标线程自己在安全点决定,所以叫「协作式」。:给目标线程一个 interrupt_flag(中断标志),主线程要它停就把这个标志置位(发「下班」信号);目标线程在自己代码里的若干 ↡协作式中断里,线程代码中主动检查中断标志的那些位置(通常封装成一个 interruption_point() 调用,放在循环里、每干完一个段落调一次)。线程跑到中断点才会发现「该停了」并退出——所以中断只在这些安全位置生效,不会打断到一半。 主动查一眼标志:没置位就接着干,置位了就抛出异常、退出循环,把手里的资源干净收尾。下面这张动画演示「线程到中断点查 flag → 主线程置位 → 线程下一个中断点查到 → 退出」:
第 1 / 4 步 · 线程跑到第 1 个中断点,检查 interrupt_flag = false → 没让下班,继续干
中断不是强杀:主线程 interrupt() 只把 interrupt_flag 置位;目标线程每跑到一个「中断点」就查一次 flag——没置位继续干,置位了就抛出/退出、干净收尾。这就是协作式中断。可暂停、单步、拖进度。
要点是:中断不是强杀。主线程只负责「按下开关」,停在哪、怎么收尾,由目标线程跑到下一个中断点时自己决定——这就是「协作式」三个字的全部含义。
上手玩一玩这三张图
这三张图都是可单步的教具,配合下一节代码一起玩比读十遍管用:
- 线程池主图
<ThreadPoolDiagram />(主图):单步盯第④拍——worker0 做完 T1 后还是它自己抓 T3,不新建线程。把这一拍看透,就懂了「复用固定那几个工人」省的到底是什么开销。 - 工作窃取
<WorkStealingDiagram />:单步看第③拍 worker2 从 worker0 的队尾偷 C,而不是抢它正在取的本端。体会「本地队列减争用 + 偷队尾均衡负载」这两层。 - 协作式中断
<InterruptibleThreadDiagram />:单步看第②拍主线程只是把 flag 置位(没碰线程本身),第③④拍线程自己跑到中断点查到才退出。理解「中断 = 发信号 + 线程到安全点自退」,不是强杀。
玩熟这三张图,下一节每一句 cv.wait / submit / try_steal / interruption_point 你都能对上图里某个节点。
代码:一步步把线程池写出来
工作线程循环:取任务 → 执行 → 再取
先看每个工作线程跑的那段循环——这是线程池的心脏。它从任务队列取任务,没活就在条件变量上阻塞等待(不空转),收到收工信号且无活时退出:
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool {
std::vector<std::thread> workers; // 固定一队工作线程
std::queue<std::function<void()>> tasks; // 任务队列
std::mutex m;
std::condition_variable cv;
bool done = false; // 析构信号
void worker_loop() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [this] { return done || !tasks.empty(); });
if (done && tasks.empty()) return; // 收工且无活就退
task = std::move(tasks.front());
tasks.pop();
}
task(); // 在锁外执行,别占着锁干活
}
}
};两个细节最关键:① cv.wait 带谓词 done || !tasks.empty()——没活就睡(防忙等),有活或要收工才醒(承接第四章「带谓词的 wait」)。② task() 在锁外执行——绝不能攥着队列锁去跑任务,否则别的工人全卡在锁上,并发就没了。
构造与析构:起固定 N 个工人,收工时 join 每一个
工人循环有了,再补上构造(起 N 个工人)和析构(让所有工人收工、join 回收)。析构这步是新手最易漏的「红线」:
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex m;
std::condition_variable cv;
bool done = false;
void worker_loop(); // 实现见上一段
public:
explicit ThreadPool(unsigned n = std::thread::hardware_concurrency()) {
if (n == 0) n = 1; // 只是默认提示;生产配置应由负载测量给出
try {
for (unsigned i = 0; i < n; ++i)
workers.emplace_back([this] { worker_loop(); });
} catch (...) {
{ std::lock_guard<std::mutex> lk(m); done = true; }
cv.notify_all();
for (auto& t : workers) t.join();
throw;
}
}
~ThreadPool() {
{ std::lock_guard<std::mutex> lk(m); done = true; } // 置收工信号
cv.notify_all(); // 叫醒所有在等任务的工人
for (auto& t : workers) t.join(); // 等每个工人收工,绝不漏 join
}
};构造失败也必须执行同一套停机流程,否则已启动 thread 的析构会 terminate。正常析构采用 drain:置 done 后 worker 继续取完队列,再退出。调用者必须保证析构发生在非 worker 所有者线程,且没有并发 submit;否则自 join 或对象生存期竞争都不成立。
让 submit 返回 future:用 packaged_task 包装
只能「丢进去不管」的池不够用——我们要能拿回结果。submit 用 std::packaged_task 把任务包起来、先取出关联的 future 还给调用方,再把 task 包成 void() 入队(承接第四章):
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <type_traits>
#include <utility>
class ThreadPool {
std::queue<std::function<void()>> tasks;
std::mutex m;
std::condition_variable cv;
bool done = false;
public:
// 提交任务,返回一张 future 凭证去取它的结果
template <typename F>
auto submit(F&& f)
-> std::future<std::invoke_result_t<std::decay_t<F>&>> {
using Fn = std::decay_t<F>;
using R = std::invoke_result_t<Fn&>;
auto task = std::make_shared<std::packaged_task<R()>>(
std::forward<F>(f));
std::future<R> fut = task->get_future();
{
std::lock_guard<std::mutex> lk(m);
if (done) throw std::runtime_error("thread pool is stopping");
tasks.push([task] { (*task)(); }); // 入队(用 shared_ptr 搬进 function)
}
cv.notify_one(); // 叫醒一个等任务的工人
return fut; // 调用方凭它 get() 取结果/异常
}
};状态检查和入队在同一把锁内,因此 shutdown 与 submit 有确定先后:先入队的任务会被 drain,后到的提交抛异常。packaged_task 只能移动,而 C++17 std::function 的目标必须可复制,所以 lambda 捕获 shared_ptr;C++23 的 move-only function wrapper 才能直接保存 move-only 任务。
#include <future>
#include <functional>
#include <queue>
#include <utility>
// 假设已有上面那个带 submit 的 ThreadPool;这里给个等价的最小桩聚焦用法
struct ThreadPool {
explicit ThreadPool(unsigned) {}
template <typename F>
auto submit(F f) -> std::future<decltype(f())> {
std::packaged_task<decltype(f())()> task(std::move(f));
auto fut = task.get_future();
task(); // 真线程池里是入队、由某个工人取出执行
return fut;
}
};
int use_pool() {
ThreadPool pool(4); // 固定 4 个工人
std::future<int> f = pool.submit([] { // 派一个返回 int 的任务
int s = 0;
for (int i = 1; i <= 100; ++i) s += i;
return s; // 返回值会被填进 f
});
return f.get(); // 取餐:没算完就阻塞,直到就绪(5050)
}任务里若抛了异常,packaged_task 会把它存进 future,在你 f.get() 时原样重新抛出——这正是用 future 接结果的另一大好处:异步任务的异常不会被吞,照样能 try/catch(对照第七节那个坑)。
这也形成 worker_loop 的边界契约:队列只接收不会把用户异常逃出 task() 的包装器。packaged_task 满足该契约;若另开 raw void 提交通道,wrapper 必须捕获并交给统一错误处理器,否则异常逃出线程入口会调用 std::terminate。
工作窃取:本地双端队列,本端取放、队尾被偷
要降争用,给每个工作线程配一条本地双端队列。本线程从本端(front)push/pop,别的线程来偷时从队尾(back)拿——两端分开,连偷取都尽量不撞:
#include <deque>
#include <functional>
#include <mutex>
#include <optional>
#include <utility>
using Task = std::function<void()>;
// 每个工作线程一条本地双端队列:本端 push/pop(无争用),队尾给别人偷
class WorkStealingQueue {
std::deque<Task> q;
mutable std::mutex m;
public:
void push(Task t) { // 本线程提交:压本端(front)
std::lock_guard<std::mutex> lk(m);
q.push_front(std::move(t));
}
std::optional<Task> try_pop() { // 本线程取活:本端拿(LIFO)
std::lock_guard<std::mutex> lk(m);
if (q.empty()) return std::nullopt;
Task t = std::move(q.front()); q.pop_front();
return t;
}
std::optional<Task> try_steal() { // 别人来偷:队尾拿(减冲突)
std::lock_guard<std::mutex> lk(m);
if (q.empty()) return std::nullopt;
Task t = std::move(q.back()); q.pop_back();
return t;
}
};本地 LIFO 倾向先执行刚产生、仍有缓存局部性的子任务,窃贼从另一端拿较老、通常更粗的任务。这个教学实现整条 deque 仍由一把 mutex 保护,本端和偷取会互斥;收益来自把一把全局锁分散成每 worker 一把锁。真正的 Chase-Lev deque 才会让 owner 操作与 thief CAS 具有更强的端点并发性质。
#include <functional>
#include <optional>
#include <thread>
#include <vector>
using Task = std::function<void()>;
struct WorkStealingQueue {
std::optional<Task> try_pop();
std::optional<Task> try_steal();
};
std::vector<WorkStealingQueue*> all_queues; // 所有工人的本地队列
thread_local WorkStealingQueue* my_queue; // 本线程自己的那条
// 工人每轮:先吃自己的,空了再去别人队尾偷
void run_pending_task() {
if (auto t = my_queue->try_pop()) { // ① 先取自己本端的活
(*t)();
return;
}
for (auto* other : all_queues) { // ② 自己空了,遍历同事
if (other == my_queue) continue;
if (auto t = other->try_steal()) { // 从同事队尾偷一张
(*t)();
return;
}
}
std::this_thread::yield(); // 完整池应阻塞等待新任务,避免持续轮询
}all_queues 必须在线程启动前建好并活到所有 worker join 之后,不能在遍历时重分配。空闲 worker 的生产实现应在全局条件变量或计数信号上阻塞,提交任务时唤醒;只靠 yield 轮询仍会浪费 CPU。
协作式中断:interrupt_flag + interruption_point
最后是停线程。先定义中断标志和「中断点」——一个原子布尔 + 一个主动检查它的函数:
#include <atomic>
#include <stdexcept>
// 中断信号:一个原子布尔标志(每个可中断线程各有一个)
class interrupt_flag {
std::atomic<bool> flag{false};
public:
void set() { flag.store(true, std::memory_order_relaxed); } // interrupt() 调它置位
bool is_set() const { return flag.load(std::memory_order_relaxed); }
};
// 抛出它表示「收到中断、正在退出」——可被 catch 做清理
struct thread_interrupted : std::runtime_error {
thread_interrupted() : std::runtime_error("interrupted") {}
};
// 每个可中断线程一个;指向自己那面 flag
thread_local interrupt_flag* this_thread_interrupt_flag = nullptr;
// 中断点:线程主动「抬头看一眼信号」,置位了就抛出退出
void interruption_point() {
if (this_thread_interrupt_flag && this_thread_interrupt_flag->is_set())
throw thread_interrupted();
}interrupt_flag 是那盏「下班」指示灯;interruption_point() 是线程主动抬头看灯的动作。this_thread_interrupt_flag 用 thread_local,让每个线程指向自己那盏灯。业务循环里只需每干完一个段落就调一次 interruption_point():
#include <atomic>
class interrupt_flag {
std::atomic<bool> flag{false};
public:
void set() { flag.store(true, std::memory_order_relaxed); }
bool is_set() const { return flag.load(std::memory_order_relaxed); }
};
thread_local interrupt_flag* this_thread_interrupt_flag = nullptr;
struct thread_interrupted {};
void interruption_point() {
if (this_thread_interrupt_flag && this_thread_interrupt_flag->is_set())
throw thread_interrupted();
}
bool has_more_work();
void do_one_chunk();
void cleanup();
// 业务循环:每干完一块就到「中断点」自查一次——这是协作式的关键
void worker_body() {
try {
while (has_more_work()) {
do_one_chunk(); // 干一个段落的活
interruption_point(); // 抬头看信号:置位了就在这里抛出退出
}
} catch (thread_interrupted const&) {
cleanup(); // 收到中断:干净收尾(释放资源等)
}
}主线程置位后,运行中的任务要到下一个 interruption_point 才退出。原子标志不会自动唤醒 condition_variable:若目标可能睡在 wait 中,请求停止必须在该 wait 的状态锁下记录 stop,再 notify_all,谓词检查 stop || condition;否则线程可能永远睡着。C++20 的 std::jthread 与 stop_token 标准化了停止请求和部分可中断等待,本章 C++17 版本仍需手工维护标志、唤醒对象与其生存期。
容易踩的坑
小结
- 线程池预先固定一队工作线程围着一个线程安全任务队列循环「取任务 → 执行 → 再取」,复用线程,省去频繁创建/销毁线程的开销
- shutdown 与 submit 共用状态锁:关闭后拒绝新任务,唤醒 worker 排空队列,再由非 worker 所有者 join 全部线程
- packaged_task 让 future 承接值或异常;池内任务依赖不能占满 worker 阻塞等待,可帮助执行、隔离资源或调度 continuation
- 工作窃取以每 worker 本地 LIFO 队列分散全局锁,thief 从另一端取旧任务;教学 mutex deque 仍有局部互斥
- 协作式中断要求任务检查标志;若线程睡在 condition_variable,还必须记录停止状态并 notify,不能只置原子位
练习
问题 1(改代码型) 下面线程池没有析构,worker 永不退出。请补充 drain 关闭协议,并说明关闭开始后 submit 应怎样处理、漏 notify_all 或 join 会怎样。
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class LeakyPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex m;
std::condition_variable cv;
public:
explicit LeakyPool(unsigned n) {
for (unsigned i = 0; i < n; ++i)
workers.emplace_back([this] {
while (true) { // ← 永不退出
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [this] { return !tasks.empty(); });
auto t = std::move(tasks.front());
tasks.pop();
lk.unlock();
t();
}
});
}
// ← 没有析构:worker 永远在跑,pool 一销毁就访问已毁资源 / terminate
};问题 2(C 型分析题) 4 个 worker 各运行一个父任务,父任务各提交子任务并立刻 get,整个池卡死。请画出依赖等待图,并给出至少两种保留进展的修法。
问题 3(独立实现型) 给简单池增加返回 future 的 submit:用 packaged_task 传值或异常,处理 C++17 move-only task 与 copyable std::function 的冲突,并在池停止后拒绝提交。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 线程池(thread pool)
长期持有一组 worker,把多个任务调度到这些线程执行。池大小由 CPU、阻塞比例、任务粒度和共存负载共同决定;完整协议还包括容量、提交关闭、取消、结果完成与 join。
- 工作线程(worker thread)
线程池里那些预先创建好、长期存活的线程。每个工作线程跑同一段循环:从任务队列取一个任务、执行它、再回头取下一个;没活时就在条件变量上阻塞等待(不空转占 CPU)。它们被反复复用,不随任务来去而创建销毁——就是那几个「常备帮厨」。详见本章「工作线程循环」一节。
- 任务队列(task queue)
线程池里存放待执行任务的共享队列。提交任务就是把任务入队,工作线程从队列取出执行。因为多个线程会并发地入队/出队,它必须是线程安全的(用第七章那种带锁或条件变量的队列)。像后厨那个公用的「单子筐」。详见本章「工作线程循环」一节。
- 双端队列(deque)
double-ended queue 的简称,一种两端都能 push/pop 的队列。工作窃取里每个工作线程持有一条本地 deque:owner 常从本端 LIFO 取放,thief 从另一端取较老任务。若实现仍以一把 mutex 保护整个 deque,两端操作依然互斥;主要收益是分散全局队列争用。
- 工作窃取(work stealing)
一种线程池负载均衡策略:每个工作线程有自己的一条本地双端队列,平时只动自己的队列(减少对单一全局队列的争用);一旦自己队列空了,就去别的忙碌线程的队列「另一端(队尾)」偷一个任务来做。这样既保留本地队列的低争用,又让空闲线程不白等、自动均衡负载。像帮厨自己筐空了就去隔壁忙不过来的同事筐尾巴上偷一张活干。详见本章「减少争用
- 工作窃取」一节。
- 协作式中断(cooperative interruption)
停止一个线程的安全方式:不强制杀死它,而是给它「置一个中断标志」当作信号;目标线程在自己代码里的若干「中断点」主动检查这个标志——没置位就继续干,置位了就主动抛出异常 / 退出循环、做完清理再走。停不停、何时停由目标线程自己在安全点决定,所以叫「协作式」。像给帮厨发「下班」信号,他干到一个段落点抬头看见才收工,不是被当场拽走(强杀)。详见本章「协作式中断」一节。
- 中断点(interruption point)
协作式中断里,线程代码中主动检查中断标志的那些位置(通常封装成一个
interruption_point()调用,放在循环里、每干完一个段落调一次)。线程跑到中断点才会发现「该停了」并退出——所以中断只在这些安全位置生效,不会把线程打断到一半。像帮厨干完一道菜抬头看一眼「下班」灯的那个动作。详见本章「协作式中断」一节。