第9章:线程池
为什么使用线程池?
在多线程编程中,手动创建和销毁线程(Thread)会带来不小的开销:每次 CreateThread 都需要在内核和用户态之间切换,线程栈也要占据地址空间。线程池(Thread Pool)正是为解决这一问题而生的——它是一组预先创建好、随时待命的线程,应用程序只需将任务"投递"给池,池中的空闲线程就会取出任务并执行。
Windows 提供的线程池机制有三个核心优势:
- 客户端无需显式创建或终止线程:调用者只管提交工作项,不关心哪个线程在跑。
- 已完成任务的线程返回池中复用而非销毁:避免反复创建/销毁线程的开销。
- 池中线程数量可根据负载动态调整:忙时增加线程提高吞吐,闲时收缩线程节省资源。
从 Windows Vista 起,线程池 API 大幅增强,并开始支持私有线程池(Private Thread Pool)——一个进程可以拥有多个独立的线程池。Windows 使用名为 TpWorkerFactory 的内核对象管理线程池。即便像记事本这样简单的程序,也可能在线程池线程(起始函数为 ntdll!TppWorkerThread)上运行,闲置一段时间后这些线程会自动消失。
线程池工作回调函数
基本提交方式
最简单的线程池 API 是 TrySubmitThreadpoolCallback,它将一个回调函数提交给默认线程池:
BOOL TrySubmitThreadpoolCallback(
PTP_SIMPLE_CALLBACK pfnCallback,
PVOID pvContext,
PTP_CALLBACK_ENVIRON pcbe
);参数说明:
pfnCallback:回调函数指针,类型为PTP_SIMPLE_CALLBACK。pvContext:上下文值(void*),可传递任意数据给回调。pcbe:可选的回调环境参数,可设为NULL使用默认配置。
回调函数的原型为:
VOID CALLBACK SimpleCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Context
);Instance:不透明的实例指针(PTP_CALLBACK_INSTANCE),可用于后续对回调的精细控制。Context:提交时传入的pvContext。
TrySubmitThreadpoolCallback 成功返回 TRUE,失败通常只在极端内存压力下发生。提交后无法直接取消,也无法直接获知完成时机——这是它最简洁也最受限的地方。
简单工作应用程序
下面的演示程序展示了线程池的动态特性:点击按钮提交工作项,回调通过 PostMessage 向 UI 线程报告自己在哪个线程 ID 上执行。
#include <windows.h>
#include <stdio.h>
#define WM_WORK_DONE (WM_USER + 1)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context) {
HWND hWnd = (HWND)Context;
DWORD tid = ::GetCurrentThreadId();
::PostMessage(hWnd, WM_WORK_DONE, 0, tid);
}
// 获取进程中的线程数(通过 Tool Help API 遍历进程快照)
DWORD GetProcessThreadCount(DWORD pid) {
HANDLE hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
DWORD count = 0;
THREADENTRY32 te32 = { sizeof(te32) };
if (::Thread32First(hSnapshot, &te32)) {
do {
if (te32.th32OwnerProcessID == pid)
count++;
} while (::Thread32Next(hSnapshot, &te32));
}
::CloseHandle(hSnapshot);
return count;
}
// ... WndProc 在 WM_WORK_DONE 中打印线程数,按钮点击时调用 TrySubmitThreadpoolCallback这个程序运行时可以观察到:线程数随负载增加而增多,闲置一段时间后自动减少。线程数的获取通过 Tool Help API 遍历进程快照实现。
控制工作项
如果需要更多控制(如等待完成或取消),可以显式创建工作项对象,而不是使用轻量的 TrySubmitThreadpoolCallback。
创建工作项:CreateThreadpoolWork
PTP_WORK CreateThreadpoolWork(
PTP_WORK_CALLBACK pfnWorkCallback,
PVOID pvContext,
PTP_CALLBACK_ENVIRON pcbe
);返回 PTP_WORK 不透明指针。回调原型比 SimpleCallback 多一个 Work 参数:
VOID CALLBACK WorkCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_WORK Work
);提交工作项:SubmitThreadpoolWork
VOID SubmitThreadpoolWork(PTP_WORK Work);提交工作(可多次提交同一对象),返回 void,不会失败。
等待/取消:WaitForThreadpoolWorkCallbacks
VOID WaitForThreadpoolWorkCallbacks(PTP_WORK Work, BOOL fCancelPendingCallbacks);- 当
fCancelPendingCallbacks为FALSE时,等待所有已提交的回调完成。 - 当
fCancelPendingCallbacks为TRUE时,取消尚未开始执行的排队回调,但不会强制终止已运行的回调。
销毁:CloseThreadpoolWork
VOID CloseThreadpoolWork(PTP_WORK Work);释放工作对象。注意:CloseThreadpoolWork 不会等待未完成的回调,通常应先调用 WaitForThreadpoolWorkCallbacks。
WIL RAII 包装器
Windows Implementation Library(WIL)提供了 wil::unique_threadpool_work 等 RAII 包装器,其中 nowait 变体在超出作用域时仅关闭对象而不等待,适合不关心回调是否完成的场景。
MD5 计算器应用程序
这是对第 7 章 MD5 计算器应用的线程池改造。原方案每次计算都创建一个新线程,效率不高。使用线程池有两种替换方式:
方式一:TrySubmitThreadpoolCallback
最简单的替换方案。本质是用线程池任务代替 CreateThread,失败的可能性更小,因为"比创建一个新线程需要的资源更少"。
方式二:CreateThreadpoolWork 配合 WIL 包装器
更精细的控制方式,适合需要等待完成或取消的场景。但如果每次提交都需要不同的上下文,手动创建工作项的好处有限——此时 TrySubmitThreadpoolCallback 更简洁。
设计考量:选择哪种方式取决于你是否需要等待所有计算完成。如果无需等待,
TrySubmitThreadpoolCallback足够;如果需要知道何时全部完成以收集结果,则应使用CreateThreadpoolWork+WaitForThreadpoolWorkCallbacks。
线程池等待回调函数
线程池等待(Wait Callback)比手动创建线程等待更高效,因为"同一个线程池线程可以等待应用程序以及可能其他 Windows API 和库提交的多个对象",单个线程可通过 WaitForMultipleObjects 同时等待最多 64 个对象。
创建等待对象:CreateThreadpoolWait
PTP_WAIT CreateThreadpoolWait(
PTP_WAIT_CALLBACK pfnWaitCallback,
PVOID pvContext,
PTP_CALLBACK_ENVIRON pcbe
);回调原型:
VOID CALLBACK WaitCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_WAIT Wait,
TP_WAIT_RESULT WaitResult
);WaitResult 可以是 WAIT_OBJECT_0(内核对象已发出信号)或 WAIT_TIMEOUT(超时)。
设置等待:SetThreadpoolWait
VOID SetThreadpoolWait(
PTP_WAIT Wait,
HANDLE h,
PFILETIME pftTimeout
);h:等待句柄。设为NULL会停止排队新回调。pftTimeout:超时时间(FILETIME 格式,NULL表示无限期等待)。
对同一 PTP_WAIT 再次调用 SetThreadpoolWait 会取消当前等待并用新信息替换。还有一个 SetThreadpoolWaitEx 变体返回 BOOL 表示替换状态。
管理与清理
VOID WaitForThreadpoolWaitCallbacks(PTP_WAIT Wait, BOOL fCancelPendingCallbacks);
VOID CloseThreadpoolWait(PTP_WAIT Wait);典型场景:在"关闭事件"模式中,进程不再创建专用等待线程,而是通过 CreateThreadpoolWait + SetThreadpoolWait 让线程池代为等待共享事件。当事件发出信号时,回调自动执行关闭操作,主线程只需等待回调完成即可。
线程池定时器回调函数
第 8 章介绍的可等待定时器(Waitable Timer)需要等待操作或 APC 来触发动作,而线程池定时器"可以直接从线程池中调用回调",使用更方便。
创建定时器:CreateThreadpoolTimer
PTP_TIMER CreateThreadpoolTimer(
PTP_TIMER_CALLBACK pfnTimerCallback,
PVOID pvContext,
PTP_CALLBACK_ENVIRON pcbe
);回调原型:
VOID CALLBACK TimerCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_TIMER Timer
);设置定时器:SetThreadpoolTimer
VOID SetThreadpoolTimer(
PTP_TIMER Timer,
PFILETIME pftDueTime,
DWORD msPeriod,
DWORD msWindowLength
);pftDueTime:到���时间(FILETIME 格式)。设为NULL停止排队新到期请求。msPeriod:周期间隔(毫秒),设为 0 表示一次性定时器。msWindowLength:可接受容差,用于系统合并多个定时器以节省电源。再次调用会用新信息替换旧定时器。
相关辅助函数:
VOID SetThreadpoolTimerEx(PTP_TIMER Timer, PFILETIME pftDueTime, DWORD msPeriod, DWORD msWindowLength);
BOOL IsThreadpoolTimerSet(PTP_TIMER Timer);
VOID WaitForThreadpoolTimerCallbacks(PTP_TIMER Timer, BOOL fCancelPendingCallbacks);
VOID CloseThreadpoolTimer(PTP_TIMER Timer);简单定时器示例
将第 8 章的可等待定时器示例重写为线程池版本:
#include <windows.h>
#include <stdio.h>
VOID CALLBACK OnTimer(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer) {
static LONG count = 0;
printf("Tick %d on thread %u\n",
(int)::InterlockedIncrement(&count),
::GetCurrentThreadId());
}
int main() {
// 确保 LONG64 可与 FILETIME 同等对待
static_assert(sizeof(LONG64) == sizeof(FILETIME), "Size mismatch");
PTP_TIMER hTimer = ::CreateThreadpoolTimer(OnTimer, nullptr, nullptr);
if (!hTimer) return 1;
LONG64 dueTime = -10000 * 1000LL; // 相对时间:100 毫秒后首次触发
::SetThreadpoolTimer(hTimer, (FILETIME*)&dueTime, 1000, 0);
// 周期 1 秒(1000ms),无容差
printf("Main thread sleeping 10 seconds...\n");
::Sleep(10000);
// 停止定时器并等待所有回调完成
::SetThreadpoolTimer(hTimer, nullptr, 0, 0);
::WaitForThreadpoolTimerCallbacks(hTimer, FALSE);
::CloseThreadpoolTimer(hTimer);
printf("Done.\n");
return 0;
}要点:
- 负的
dueTime值(-10000 * 1000LL)表示相对时间(100 毫秒后首次触发)。 msPeriod设为 1000 表示之后每 1 秒触发一次。msWindowLength设为 0 表示不要求容差合并。- 使用
static_assert确保LONG64与FILETIME大小一致,可安全地相互转换。
线程池 I/O 回调函数
线程池 I/O 回调用于为异步 I/O 操作提供服务。这部分内容将在第 11 章「文件和设备 I/O」中详细讨论,此处仅作概念上的引入。
线程池实例操作
PTP_CALLBACK_INSTANCE 是不透明的实例参数,出现在所有回调的原型中。它配合以下函数提供对回调运行时行为的精细控制:
CallbackMayRunLong
BOOL CallbackMayRunLong(PTP_CALLBACK_INSTANCE Instance);提示线程池此回调可能长时间运行,池应生成新线程来处理下一个请求。返回 TRUE 表示能生成新线程,FALSE 表示不能(例如已达最大线程数限制)。应在回调开始时调用,让线程池提前调度。
回调返回前自动执行的操作
以下四个函数在回调返回前自动执行特定操作,无需手动调用:
VOID SetEventWhenCallbackReturns(PTP_CALLBACK_INSTANCE Instance, HANDLE hEvent);
VOID ReleaseSemaphoreWhenCallbackReturns(PTP_CALLBACK_INSTANCE Instance, HANDLE hSem, DWORD dwReleaseCount);
VOID ReleaseMutexWhenCallbackReturns(PTP_CALLBACK_INSTANCE Instance, HANDLE hMutex);
VOID LeaveCriticalSectionWhenCallbackReturns(PTP_CALLBACK_INSTANCE Instance, LPCRITICAL_SECTION lpCritSect);这些函数保证"即使回调以异常方式退出,操作也会执行",因此比在回调末尾手动调用对应的 Release/Leave 更可靠。
FreeLibraryWhenCallbackReturns
VOID FreeLibraryWhenCallbackReturns(PTP_CALLBACK_INSTANCE Instance, HMODULE hModule);回调返回前自动卸载 DLL。这解决了一个经典难题:回调自己调用 FreeLibrary 会导致致命错误,因为回调函数的代码就在该 DLL 中,卸载后返回地址将不再有效。通过线程池代为卸载,回调返回后 DLL 才被安全释放。
DisassociateCurrentThreadFromCallback
VOID DisassociateCurrentThreadFromCallback(PTP_CALLBACK_INSTANCE Instance);告知线程池该回调已完成"重要工作",使其他等待的线程可以继续(例如 WaitForThreadpoolWorkCallbacks 可以返回),即使此回调仍在执行一些收尾工作。这给调用者提供了"提前告知完成"的手段。
回调环境
PTP_CALLBACK_ENVIRON 是每个线程池对象创建函数的最后一个参数,提供了一种统一的方式来定制回调行为。
初始化与销毁
VOID InitializeThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
VOID DestroyThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);InitializeThreadpoolEnvironment 清零所有字段并将 Version 字段设为适当值(Win7+ 为 3,Vista 为 1)。DestroyThreadpoolEnvironment 当前为空操作,但建议调用以兼容未来版本。
定制函数
初始化后可通过以下函数设置环境属性:
| 函数 | 作用 |
|---|---|
SetThreadpoolCallbackPool | 设置回调使用的线程池对象(非默认池) |
SetThreadpoolCallbackPriority | 设置回调优先级(TP_CALLBACK_PRIORITY_HIGH / NORMAL / LOW) |
SetThreadpoolCallbackRunsLong | 提示回调为长时间运行 |
SetThreadpoolCallbackLibrary | 表明回调属于某个 DLL,池会保持 DLL 加载,并帮助防止加载器锁死锁 |
SetThreadpoolCallbackCleanupGroup | 将回调与清理组关联 |
优先级设置在 Windows 7 / Server 2008 R2 中引入,高优先级回调保证先于低优先级回调启动。
私有线程池
默认情况下,进程有一个单一且不可销毁的默认线程池。通过回调环境配合 SetThreadpoolCallbackPool,可将回调定向到私有线程池(Private Thread Pool),实现线程隔离和资源精细控制。
创建与销毁
PTP_POOL CreateThreadpool(PVOID Reserved); // Reserved 须设为 NULL
VOID CloseThreadpool(PTP_POOL ptpp);线程数控制
VOID SetThreadpoolThreadMaximum(PTP_POOL ptpp, DWORD cthrdMost);
VOID SetThreadpoolThreadMinimum(PTP_POOL ptpp, DWORD cthrdMin);SetThreadpoolThreadMaximum:设置最大线程数,默认值为 512。SetThreadpoolThreadMinimum:设置最小线程数,默认值为 0。当设为大于零的值时,池会预创建这些线程以备即时使用。
栈信息控制
typedef struct _TP_POOL_STACK_INFORMATION {
SIZE_T StackReserve;
SIZE_T StackCommit;
} TP_POOL_STACK_INFORMATION;
VOID SetThreadpoolStackInformation(PTP_POOL ptpp, PTP_POOL_STACK_INFORMATION ptpsi);
VOID QueryThreadpoolStackInformation(PTP_POOL ptpp, PTP_POOL_STACK_INFORMATION ptpsi);默认栈大小来自 PE 头中的设置——提交 4KB、保留 1MB。可通过 SetThreadpoolStackInformation 覆盖这些值。默认的最大/最小值没有文档化的查询函数,但可以通过原生 API NtQueryInformationWorkerFactory 实现。
使用场景:当不希望某个模块的线程池任务影响其他模块的性能时,可为该模块创建独立的私有线程池,限制其最大线程数。
清理组(Cleanup Groups)
清理组简化了多个线程池对象的生命周期管理,它"跟踪与其关联的所有回调,这样就可以一次性关闭它们"。
创建清理组
PTP_CLEANUP_GROUP CreateThreadpoolCleanupGroup();关联到环境
VOID SetThreadpoolCallbackCleanupGroup(
PTP_CALLBACK_ENVIRON pcbe,
PTP_CLEANUP_GROUP ptpcg,
PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);ptpcg:清理组对象。pfng:可选的撤销回调(PTP_CLEANUP_GROUP_CANCEL_CALLBACK),当回调被取消时调用。
撤销回调的原型:
VOID CALLBACK CleanupGroupCancelCallback(
PVOID ObjectContext,
PVOID CleanupContext
);其中 ObjectContext 是原始回调的上下文,CleanupContext 是额外传递的清理上下文。
关闭成员与清理组
VOID CloseThreadpoolCleanupGroupMembers(
PTP_CLEANUP_GROUP ptpcg,
BOOL fCancelPendingCallbacks,
PVOID pvCleanupContext
);
VOID CloseThreadpoolCleanupGroup(PTP_CLEANUP_GROUP ptpcg);CloseThreadpoolCleanupGroupMembers 等待所有未完成回调完成;当 fCancelPendingCallbacks 为 TRUE 时,取消未开始的回调,并为每个被取消项调用撤销回调(传入原始上下��� ObjectContext 和 pvCleanupContext)。
重要限制:清理组仅适用于私有线程池,因为默认线程池无法被销毁——这是合理的,清理组的存在意义正是配合可销毁的私有线程池。
练习
- 使用线程池实现第 5 章的曼德布洛特(Mandelbrot)计算。原先的方案手动创建线程分配计算区域,请改用
CreateThreadpoolWork提交每个区域的计算工作,并使用WaitForThreadpoolWorkCallbacks等待全部完成后合并结果。 - 使用线程池实现第 8 章的练习。将原来的可等待定时器方案替换为线程池定时器,体会两种方式的差异。
总结
线程池是在高度多线程进程中提高性能和可扩展性的强大机制。本章从最简单的 TrySubmitThreadpoolCallback 开始,逐步深入到完全受控的 CreateThreadpoolWork、事件等待回调、定时器回调以及 I/O 回调。通过 PTP_CALLBACK_INSTANCE 提供运行时行为控制,通过回调环境(Callback Environment)实现优先级、线程池归属和 DLL 生命周期等定制。最后引入了私有线程池和清理组,它们共同构成了企业级应用中线程资源隔离和生命周期管理的基础设施。
核心要点回顾:
- 工作回调(Work Callback):最常用的线程池模式,从轻量级
TrySubmitThreadpoolCallback到完全可控的CreateThreadpoolWork。 - 等待回调(Wait Callback):让线程池代为等待内核对象,一个线程可同时等待最多 64 个对象。
- 定时器回调(Timer Callback):比可等待定时器更便捷,直接在池中触发回调。
- I/O 回调(I/O Callback):为异步 I/O 提供服务(详见第 11 章)。
- 回调环境(Callback Environment):统一配置回调的优先级、池归属、清理组等属性。
- 私有线程池(Private Thread Pool):实现线程资源隔离和精细控制。
- 清理组(Cleanup Group):批量管理相关回调的生命周期,支持撤销通知。
下一章将汇总与线程相关的高级功能。