更新记录(仅首版,2026年08月22日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176825)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《Win32 线程池 API ── 用 CreateThreadpoolWork 实现「不自建线程」的并发》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/win32-thread-pool-api/
- DOI(已登记存档)
- 10.5281/zenodo.22176825
- DOI(上次登记版本)
- 10.5281/zenodo.22176826
「每个客户端一条 CreateThread」「定时器再来一条」「等事件再来一条」。在原生 Windows 代码里,人们很容易为了一点小工作就多开线程。线程每多一条就要消耗一份栈和内核对象,创建和销毁本身也要花代价。
吸收这一错配的,正是操作系统自带的 Win32 线程池 API。应用把想执行的处理作为回调交出去,工作线程的管理则交给操作系统。这里说的「不自建线程」,并不是说线程不再需要,而是指应用不再每来一件工作就自己创建、自己销毁。1
本文面向用 C/C++ 编写 Windows 应用、服务和 DLL 的开发者,按如何取舍 → 选择对象 → 实现 work → 回调与关闭处理的注意事项的顺序展开。讲的是 Windows Vista 上全面翻新的 CreateThreadpoolWork 系列 API。代码只是展示用法骨架的节选,队列之类的应用特有部分已省略。
1. 先说结论
要大量处理短命工作,就不要管理线程,而要管理工作。但工作什么时候停、什么时候可以释放,得由应用自己设计。
判断时看三条轴。
- 先挑工具。标准 C++ 或 .NET 的工具够用,就用它们。想在 Win32 上统一定时器、等待和 I/O 完成,或者想拆分线程池,才轮到这套 API 出场。
- 按工作的单位交出去。使用新 API 的 work、timer、wait、io,而需要线程优先级、COM 的 STA 等线程固有状态的工作,仍留在专用线程上。
- 把结束一并设计进去。基本流程是「停止提交 → 等待完成 → 关闭」。回调里不要长时间阻塞,不要同步等待同一个池上的工作,返回前把线程状态复原。
想先跑起来的话,可以从第 4 章的 work 示例读起,再确认第 5 章的回调纪律。多个对象的批量结束在第 6 章,DLL 的卸载在第 7 章。
2. 如何取舍 ── 线程池、专用线程、标准库
2.1 适合线程池的是短命、大量、以等待为主的工作
线程池是一组由操作系统管理的工作线程。工作线程依次执行回调,操作系统按负载调节数量。把自己创建又销毁线程的活儿交出去,就能省掉管理代码,也省掉创建与销毁的负担。1
官方文档列举的适用对象是:大量并行发出小型工作项的应用、频繁创建和销毁短命线程的应用、在后台并行处理互相独立的工作的应用、拥有专门等待内核对象或事件的线程的应用。搜索、网络 I/O,以及整理等待专用线程,都是典型场景。1
另一方面,需要改变线程优先级、需要 COM 的 STA、在进程存活期间一直运行——这类工作要放在专用线程上。判断标准是:只要能把处理跑起来就行,还是线程本身需要有「个性」。
flowchart TB
accTitle: 专用线程与线程池的取舍
accDescr: 先确认工作是否需要优先级或 STA 之类的线程个性、是否会长时间持续运行,只有两者都不成立的短命、大量、等待型工作才放到线程池上
q1{"需要优先级、STA 等个性吗?"} -->|"是"| ded["放在专用线程上"]
q1 -->|"否"| q2{"会长时间持续运行吗?"}
q2 -->|"是"| ded
q2 -->|"否"| pool["放到线程池上"]
pool -.-> ex["短命工作、等待、定时器、I/O 完成"]
图 1: 能放上线程池的只有「不需要个性、很快结束」的工作。其余仍像以前一样交给专用线程。
2.2 先想清楚标准 C++ 或 .NET 够不够用
如果 C++ 的 std::async 或 std::thread 的粒度就够用,标准库是第一候选。它可移植,std::async 与 future 的行为也能依据标准来把握。2
直接使用 Win32 线程池的理由是这些需求:想统一包含 timer、wait、io 在内的回调机制,想按工作种类拆分线程池并分别控制线程数,不想在 DLL 或 COM 组件里自持线程。要把「只是想并行化」和「需要 Windows 特有的控制」分开来考虑。
flowchart TB
accTitle: 用哪一层的工具来写
accDescr: 标准 C++ 的 async 或 thread 够用就用它们,需要统一定时器、等待和 I/O 完成,或需要拆分线程池并控制线程数,或不想在 DLL 与 COM 组件里自持线程时,才直接使用 Win32 线程池
q1{"标准 C++ 的工具够用吗?"} -->|"是"| std["std::async / std::thread"]
q1 -->|"否"| q2{"需要什么?"}
q2 -->|"统一 timer、wait、io"| tp["Win32 线程池"]
q2 -->|"拆分线程池、控制数量"| tp
q2 -->|"避免在 DLL 内自持线程"| tp
图 2: 拿不准就先用标准库,出现它表达不了的需求时,才是这套 API 登场的时候。
在 .NET 里承担同样角色的是 ThreadPool 和 Task,I/O 完成则与 IOCP 相连。详细的关系请参见讲 IOCP 与 .NET 线程池的文章。上层工具够用的场合,没必要一路下探到 Win32 API。
2.3 新代码请使用 Vista 之后的新 API
Win32 的线程池 API 有两代。一代是从 Windows 2000 延续下来的 QueueUserWorkItem、RegisterWaitForSingleObject 等旧 API,另一代是 Windows Vista 上全面重新设计的 CreateThreadpoolWork 系列新 API。
新 API 统一了工作线程的种类,并提供了单一的计时器队列、专用的持久线程、进程内互相独立的多个池,以及清理组。官方也把新 API 的简单、可靠、性能和灵活性列为优点。13
旧 API 还有一条结构性限制:没有办法取消已经入队的工作。新代码请使用新 API;回头审视既有代码时,可以从下面这份对照关系开始。3
flowchart TB
accTitle: 旧线程池 API 与新 API 的对应关系
accDescr: 旧 API 的 QueueUserWorkItem 对应新 API 的 work 对象,计时器队列对应 timer,已注册的等待对应 wait,BindIoCompletionCallback 对应 io
o1["QueueUserWorkItem"] --> n1["work"]
o2["计时器队列"] --> n2["timer"]
o5["已注册的等待"] --> n3["wait"]
o4["BindIoCompletionCallback"] --> n4["io"]
图 3: 从旧 API 迁移的去向是一一对应的。梳理既有代码可以从这张对照表开始。
3. 机制 ── 触发条件各不相同的四种对象
3.1 按「由什么来触发」来选择
新 API 的核心是下面四种。选择时看的不只是「处理什么」,更是想让回调由什么来触发。4
| 对象 | 创建函数 | 回调的触发条件 |
|---|---|---|
| work | CreateThreadpoolWork |
被 SubmitThreadpoolWork 提交时 |
| timer | CreateThreadpoolTimer |
到达指定的时刻或周期时 |
| wait | CreateThreadpoolWait |
内核对象变为有信号时 |
| io | CreateThreadpoolIo |
关联句柄上的异步 I/O 完成时 |
触发条件虽然不同,负责执行的却是同一个池里的那群工作线程。周期处理、事件响应、I/O 完成处理,不必各写一条专用线程,可以统一到同一套回调机制上。
flowchart TB
accTitle: 把触发条件与回调执行分开的结构
accDescr: 持有触发条件的对象让回调变为可执行,再由池里的工作线程群去执行,处理完的工作线程还会用于下一个回调,因此应用不必为每件工作创建线程
app["应用指定工作和触发条件"] --> obj["对象等待条件成立"]
obj --> ready["回调变为可执行"]
ready --> workers["池里的工作线程群执行"]
workers --> done["处理完毕后归还工作线程"]
done --> next["下一个回调继续复用"]
图 4: 把等待触发的机制和执行处理的工作线程分开,就能减少逐件工作的线程管理。
3.2 「只是睡着等待的线程」也能替换掉
线程池的效果不限于消耗 CPU 的工作。计时器会汇聚到单一的计时器队列,多个句柄的等待会汇聚到少数几条等待线程上。1
比如说,如果有 5 条线程只是为了「事件有信号时动一下」而睡着,那就可以考虑换成 5 个 wait 对象。不再各自持有睡眠的线程,而是把有信号时的处理作为回调来执行。
flowchart TB
accTitle: 把等待专用线程换成 wait 对象
accDescr: 原本每个事件各睡一条的等待专用线程,换成 wait 对象后就汇聚到池的等待线程上,只在有信号时才执行回调
old2["等待专用线程 5 条各自睡眠"] -.-> waste["消耗 5 份栈和线程"]
new2["wait 对象 5 个"] --> agg["汇聚到池的等待线程"]
agg --> cb2["仅在有信号时执行回调"]
图 5: 「只是睡着等待的线程」可以通过 wait 对象化消除掉。这是迁移到线程池时最容易上手的第一步。
4. 实现基础 ── 创建 work、提交、安全结束
4.1 先把从创建到结束走一遍
用最基础的 work 对象把用法走一遍。CreateThreadpoolWork 把回调和 context 绑在一起,SubmitThreadpoolWork 请求执行。第三个参数为 NULL 时使用进程默认的池。多数用途用这个默认池就够了。56
下面是用来展示步骤的节选,并不是可以直接编译的完整程序。WORK_QUEUE、ITEM、Enqueue、Dequeue、ProcessItem 代表应用一侧的处理。队列的互斥控制、提交方的停止、失败处理都要另行实现;如果 work 创建失败,就不要继续往后面的提交、等待、释放走。
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance,
PVOID context, PTP_WORK work)
{
// context 在创建时就固定了。每一项的数据用同步过的队列来传
WORK_QUEUE* queue = (WORK_QUEUE*)context;
ITEM* item = Dequeue(queue); // 带互斥控制地取出一件
ProcessItem(item);
}
// 1) 创建(把回调和共享的上下文=队列绑在一起)
PTP_WORK work = CreateThreadpoolWork(WorkCallback, &queue, NULL);
if (!work) { /* 用 GetLastError 做失败处理 */ }
// 2) 每放入一件就提交一次(让件数和提交次数一致)
Enqueue(&queue, item);
SubmitThreadpoolWork(work);
// 3) 先停下提交方,再等待完成(为 TRUE 时还会尝试取消尚未执行的部分)
WaitForThreadpoolWorkCallbacks(work, FALSE);
// 4) 关闭
CloseThreadpoolWork(work);
flowchart TB
accTitle: work 对象的生命周期
accDescr: 用 CreateThreadpoolWork 创建,用 SubmitThreadpoolWork 提交后回调会并行执行。结束时先停止新的提交,再用 WaitForThreadpoolWorkCallbacks 等待全部回调完成,然后用 CloseThreadpoolWork 关闭
c["用 CreateThreadpoolWork 创建"] --> s["用 SubmitThreadpoolWork 提交(可多次)"]
s --> run["回调并行执行"]
run --> stop3["停止新的提交"]
stop3 --> w["用 WaitForThreadpoolWorkCallbacks 等待完成"]
w --> cl["用 CloseThreadpoolWork 关闭"]
图 6: 结束的顺序是「停止提交 → 等待完成 → 关闭」。少了任何一步都会变成释放后访问或竞态。
4.2 work 可以复用,但 context 不会随提交而变
同一个 work 对象,即使上一次回调还没结束,也可以多次提交。每提交一次就执行一次回调,多次提交的回调会并行运行。实际使用的线程数,线程池可能出于效率考虑而作调节。7
这里要区分开的,是 work 和各个工作项的数据。传给回调的 context 在创建时就固定了。如果要用一个 work 处理 N 件不同的数据,就像上面的例子那样,把同步过的队列作为 context。数据每放入一件就提交一次,回调则从队列里取出一件。按每一项各建一个 work 对象的设计也没问题。5
flowchart TB
accTitle: 从固定的 context 取得逐项的数据
accDescr: 在创建 work 时把 context 固定为共享队列,提交方每放入一项就 Submit 一次,并行运行的各个回调再从带互斥控制的队列里逐件取出
create["创建 work 时固定 context"] -.-> queue["带互斥控制的共享队列"]
item["放入一项"] --> queue
item --> submit["每一项 Submit 一次"]
submit --> callbacks["各回调并行运行"]
callbacks --> dequeue["从队列逐件取出"]
queue --> dequeue
dequeue --> process["处理取出的项"]
图 7: 被固定的是指向队列的 context,逐项的数据则由同步过的队列来传递。
4.3 结束时,要先停止提交再等待
安全结束的顺序是:「停止新的提交 → 用 WaitForThreadpoolWorkCallbacks 等待完成 → 用 CloseThreadpoolWork 关闭」。回调所引用的内存,同样不能在确认完成之前释放。让正在执行或正在排队的处理留着不管却释放了引用目标,就会导致释放后访问。6
只想着「我调了完成等待所以安全」是不够的。只要别的线程还能调用 SubmitThreadpoolWork,等待结束之后的提交就会和 Close 撞车。要让等待成为结束处理的分界线,前提是先把提交方停下来。
WaitForThreadpoolWorkCallbacks 的第二个参数为 FALSE 时等待完成,为 TRUE 时还会指定取消尚未开始的回调。这并不意味着可以把正在执行的处理丢下不管。把多个对象一起结束的方法在第 6 章讲。
5. 回调的纪律 ── 不要占用和污染借来的线程
5.1 会耗时就先声明,并且要看返回值
线程池是按「回调会迅速返回」这一前提来调节线程数的。什么都不说就一直做长时间的处理或长时间的等待,别的回调就会被拖慢。可能耗时较长时,要用 CallbackMayRunLong 告知这种可能性,或者分到专用线程上。8
不过,光调用 CallbackMayRunLong 还不够。当线程池无法为其他回调准备工作线程时,它会返回 FALSE。别无视返回值继续阻塞,这种情况下要把处理拆开、挪到专用线程等,往「不堵住线程池」的方向倒。8
flowchart TB
accTitle: 决定长时间回调的处理方式
accDescr: 需要长时间处理或等待的工作,要么分到专用线程,要么用 CallbackMayRunLong 通知线程池,若因无法准备其他工作线程而返回 FALSE,就通过拆分或移到专用线程来避免阻塞
long["需要长时间处理或等待"] --> choice{"在哪里处理"}
choice -->|"专门处理"| dedicated["分到专用线程"]
choice -->|"在池上处理"| notify["用 CallbackMayRunLong 通知"]
notify --> result{"是否确保到了其他工作线程"}
result -->|"TRUE"| run["执行长时间的处理"]
result -->|"FALSE"| split["拆分或移到专用线程"]
图 8: 长时间处理的通知,要连同「看返回值再决定下一步」一起,才算完整的一套。
5.2 不要在工作线程里同步等待同一个池上的工作
从回调 A 里向同一个池提交工作 B,再用 WaitForThreadpoolWorkCallbacks 之类等它完成——这种设计需要留心。当所有工作线程都在「等别的工作线程的活儿」时,就没有空闲工作线程去执行 B,于是形成池饥饿型死锁。
对策不是去多留几条用来等待的工作线程,而是改写成由 B 的完成回调去提交下一件工作的延续形式。别把依赖关系写成堵住工作线程的等待。
flowchart TB
accTitle: 池饥饿死锁的结构
accDescr: 当所有工作线程都同步等待提交到同一个池的另一件工作完成时,就不存在能执行那件工作的空闲工作线程,于是所有人永远等下去,形成死锁
w1["工作线程 1:等待工作 X 完成"] --> q["排队待执行的工作 X、Y"]
w2["工作线程 2:等待工作 Y 完成"] --> q
q -.-> none["没有可执行的空闲工作线程"]
none -.-> dead["所有人永远等待(饥饿死锁)"]
图 9: 在工作线程里同步等待工作线程,就没人去执行那件被等待的工作了。
5.3 把线程状态复原之后再返回
工作线程还会用于下一个毫不相干的回调。改了优先级却不改回来、留下 COM 的初始化状态、把值留在 TLS 里、忘了释放锁——这些做法都会把影响带给下一件工作。提交到线程池的函数,以及它调用的处理,都不能假定自己运行在专用线程上。9
也有让收尾与回调结束联动的 API。比如 LeaveCriticalSectionWhenCallbackReturns 就是委托线程池在回调返回之后释放临界区。4
flowchart TB
accTitle: 不要把线程状态带给下一个回调
accDescr: 同一条工作线程会被另一个回调复用,若带着优先级、COM、TLS、锁等状态返回就会污染下一件工作,做好收尾把状态复原即可防止这种带入
first["回调 A 借用工作线程"] --> cleanup{"返回前是否做了收尾"}
cleanup -->|"否"| dirty["带着残留状态被复用"]
dirty --> impact["影响到毫不相干的 B"]
cleanup -->|"是"| clean["以原本的状态归还工作线程"]
clean --> next["下一个回调 B 使用"]
图 10: 线程是借来的,所以不只要对处理结果负责,也要对归还时的线程状态负责。
5.4 不要让未处理异常漏到工作线程之外
工作线程上的未处理异常,可能会把整个进程一起拖下水。就像给专用线程写线程函数那样,在回调的入口捕获异常并留下日志。把活儿交给线程池,并不等于连工作中发生的失败也不用管了。
6. 拆分结构 ── 自定义池与清理组
6.1 自定义池用来隔离不同种类的工作
默认池不够用时,可以用 CreateThreadpool 建立互相独立的池。用 SetThreadpoolThreadMaximum 和 SetThreadpoolThreadMinimum 设定工作线程数的上下限。10
典型的目的是隔离。为了不让「慢点也没关系的批处理」把「希望立刻响应的工作」的工作线程用光,就拆分线程池,分别给出线程数的预算。这不是单纯地增加线程,而是划分哪件工作使用哪些工作线程。
6.2 用回调环境把执行去向与收尾绑在一起
指定在哪个池上执行的,是 TP_CALLBACK_ENVIRON。初始化这个回调环境,用 SetThreadpoolCallbackPool 指定池,再传给 CreateThreadpoolWork 等函数。第 4 章例子里传 NULL 的第三个参数,这里正是传入环境的位置。5
通过同一个环境,还能把清理组也绑上去。自定义池是执行去向,清理组是收尾的单位。把这两个角色分开来看,结构就容易理清了。
flowchart TB
accTitle: 用回调环境把结构绑在一起
accDescr: 回调环境指向自定义池和清理组,用这个环境创建的 work 和 timer 会在该池上执行,并可通过清理组的批量处理统一做完成等待与释放
env["回调环境(TP_CALLBACK_ENVIRON)"] --> cp["自定义池(控制线程数)"]
env --> cg["清理组"]
env --> obj["传给 work / timer / wait / io 的创建"]
cg -.-> close["批量做完成等待与释放"]
图 11: 回调环境是在创建对象时注入「在哪个池上跑、由谁来收尾」的机制。
6.3 把多个对象的完成等待和释放合在一起
当模块里的 work、timer 等越来越多,结束处理就会变成「逐个等待、逐个关闭」的一长串。用 CreateThreadpoolCleanupGroup 建一个组,让经由回调环境创建的对象都归属其中,就能用一次 CloseThreadpoolCleanupGroupMembers 把所属对象的完成等待和释放统一做完。46
这里的目标同样是不把正在执行的回调丢下不管。第 4 章那种逐个结束 work 的形式,和按组结束的形式,可以按要管理的对象数量来分别选用。
7. 从 DLL 里使用时 ── 不要在回调之前卸载
7.1 完成等待要放在显式的关闭函数里,而不是 DllMain
DLL 里最危险的,是回调的代码还可能被执行,那个 DLL 却已经被卸载。执行已被卸载的代码,就是访问违例。
基本做法是:在 DLL 的显式关闭函数里停止提交,等待回调完成,关闭对象,然后再卸载。无论用逐个的等待函数,还是用第 6 章的清理组,这一步完成确认都不能省。6
这个等待不能放在 DllMain 里。因为它与加载器锁的关系会引发另一种死锁。详情在讲 DllMain 与加载器锁的文章里有说明。
7.2 FreeLibraryWhenCallbackReturns 要与提交前的引用获取配对
「这个回调是最后一件工作,希望它返回之后就放开 DLL 的引用」——这种场合有 FreeLibraryWhenCallbackReturns。4
不过,光靠这个 API 无法防止回调开始之前的卸载。它做的事,是在正在执行的回调返回时放开一个模块引用。用法是配对的:提交前用 GetModuleHandleEx 为这次处理获取模块引用,再由回调用这个 API 放开。
flowchart TB
accTitle: 在关闭函数里关闭 DLL 与从回调归还引用
accDescr: 基本做法是在不是 DllMain 的关闭函数里停止提交、做完成等待与释放之后再卸载 DLL,而由最后一个回调归还引用的设计,则要在提交前用 GetModuleHandleEx 获取引用,与 FreeLibraryWhenCallbackReturns 在返回后的释放配成一对
shutdown["显式的关闭函数"] --> stop["停止提交、完成等待、释放"]
stop --> unload["之后再卸载 DLL"]
note["不要在 DllMain 里等待"] -.-> shutdown
acquire["提交前获取模块引用"] --> submit["提交回调"]
submit --> callback["在回调内预约释放"]
api["FreeLibraryWhenCallbackReturns"] -.-> callback
callback --> returned["返回之后释放一个引用"]
图 12: 不要把关闭函数里的完成等待,与为回调获取的引用的释放混为一谈;两者都要先把 DLL 的生命周期设计好。
8. 小结 ── 先从 work 开始,连同结束处理一起替换
Win32 线程池是把原生代码的并发从「创建线程」改为「把工作作为回调交出去」的基础。它能整理掉短命工作和等待专用线程,把线程管理交给操作系统。
引入可以是分阶段的。先用 work,把创建、提交、停止提交、完成等待、释放做成一套。接着把等待专用线程换成 wait,把定时器专用线程换成 timer。等到有需要时,再考虑 io 的统一和线程池的拆分。
flowchart TB
accTitle: 把既有代码分阶段迁到线程池
accDescr: 先梳理既有的自建线程,把适合标准库或专用线程的工作留下,再把适合线程池的工作连同停止提交和完成等待一起迁过去,然后分阶段把等待换成 wait、把定时器换成 timer
inventory["梳理既有的自建线程"] --> choose{"是否适合线程池"}
choose -->|"否"| keep["选择标准库或专用线程"]
choose -->|"是"| work["把 work 和结束处理成套迁移"]
work --> more["等待换成 wait、定时器换成 timer"]
more --> optional["按需做 I/O 统一与池拆分"]
图 13: 不只是减少线程,在每个阶段都把结束处理配齐,才是分阶段迁移的基本功。
要坚持到最后的,是不长时间阻塞、不同步等待同一个池、不弄脏线程状态这三条纪律。若是 DLL,还要再防住与卸载之间的竞态。标准 C++ 和 .NET 的工具够用的地方就交给它们,需要 Windows 特有的统一与控制的地方,再用这套 API。
相关文章
- Windows I/O 的深层(第 3 回)── I/O 完成端口(IOCP)与 .NET 线程池:async/await 的地下室
- 多线程实务最佳实践 C++ 篇
- 多线程实务最佳实践 C 语言篇
- 虚假唤醒 ── 条件变量「没有通知也会醒来」的原因与 Windows 上正确的等待方式
- DllMain 与加载器锁 ── 「DLL 初始化里什么都别做」的真正理由
- Windows 上应优先使用事件等待而非 Sleep(1) 的理由
相关咨询领域
小村软件合同公司承接线程增生的原生代码向线程池迁移的设计、C++ 应用与 DLL 并发处理的设计评审,以及池饥饿和回调引发的挂起、崩溃的原因调查。从梳理既有代码开始咨询也可以。
参考链接
-
Microsoft Learn, Thread Pools. 关于线程池是代替应用高效执行异步回调的一组工作线程,关于适合的应用类型(大量并行发出小型工作项、频繁创建销毁短命线程、并行处理独立工作、独占等待内核对象等),以及关于 Vista 上的全面重新设计(统一工作线程种类、单一计时器队列、专用持久线程、清理组、进程内多个池、新 API)。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, <future>. 关于标准库以 std::async 和 future 提供了以任务为单位的异步执行,从而不必直接管理线程也能写出并发处理。 ↩
-
Microsoft Learn, Thread Pooling. 关于旧线程池 API(QueueUserWorkItem、计时器队列、已注册的等待、BindIoCompletionCallback)的结构,关于没有办法取消已入队的工作,以及关于文中明确写出 Vista 引入的新线程池 API 更简单且在可靠性、性能、灵活性上更优。 ↩ ↩2
-
Microsoft Learn, threadpoolapiset.h header. 关于包含 CreateThreadpoolWork、CreateThreadpoolTimer、CreateThreadpoolWait、CreateThreadpoolIo 四种对象创建函数、清理组(CreateThreadpoolCleanupGroup),以及与回调完成联动的收尾(LeaveCriticalSectionWhenCallbackReturns、FreeLibraryWhenCallbackReturns 等)在内的函数一览。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). 关于用回调函数和上下文指针创建 work 对象,关于第三个参数 TP_CALLBACK_ENVIRON 可以指定回调的执行环境(所属的池等),以及为 NULL 时在默认环境下执行。 ↩ ↩2 ↩3
-
Microsoft Learn, Using the Thread Pool Functions. 关于用 CreateThreadpoolWork 创建、用 SubmitThreadpoolWork 提交、用 WaitForThreadpoolWorkCallbacks 等待完成、用 CloseThreadpoolWork 关闭的基本步骤,以及把自定义池与回调环境、清理组组合起来的构成示例。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). 关于同一个 work 对象可以不等先前的回调完成就多次提交、回调会并行执行,以及线程池可能出于效率而调节(限流)线程数。 ↩
-
Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). 关于把当前回调可能长时间运行这一点通知线程池,作为线程池判断是否为其他回调确保线程的依据,以及关于长时间的回调在可能时也应考虑使用专用线程。 ↩ ↩2
-
Microsoft Learn, Thread Pooling. 关于提交到线程池的工作项以及它调用的函数必须是线程池安全的,不得假定执行线程是专用的持久线程,以及应避免使用 TLS 和需要持久线程的异步调用。 ↩
-
Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). 关于可以对用 CreateThreadpool 创建的池设定工作线程数的上限(下限用 SetThreadpoolThreadMinimum)。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
DllMain 与加载器锁——“DLL 初始化里什么都别做”的真正原因
为什么不能在 DllMain 里调用 LoadLibrary 或与线程同步。本文依据一手资料,从串行化全部 DLL 通知的加载器锁机制,讲到死锁成立的典型场景、延迟初始化等正确设计,以及挂起的调查步骤。
参数为什么会坏掉 ── Windows 命令行参数的规则
在 Windows 上并不存在参数的数组,传给 CreateProcess 的是一条字符串,切分由接收方完成。本文讲解 CommandLineToArgvW・CRT・.NET 的切分规则,以及 .NET 的 ArgumentList 与 C++ 中正确的组装方法。
父进程消失之后还剩下什么 —— 用 Job Object 圈养子进程
为什么强制结束 UI 之后,SDK 的辅助进程仍然残留,一直占着摄像头或 COM 端口?本文从测量应用的视角,讲解如何用 Job Object 把进程树变成一个单位,并借助 KillOnJobClose 与完成端口来设计子进程的寿命。
命名管道实务 ── 从设计到安全,吃透 Windows 进程间通信的标准方案
以实务视角讲解 Windows 进程间通信的标准方案——命名管道。依据一手资料梳理字节模式与消息模式的取舍、同时接纳多个客户端的服务器结构、ACL 与模拟的安全设计,以及 .NET 的命名管道流。
“无响应”的真相——Windows 如何判定应用卡死,以及不会卡死的设计
Windows 的“无响应”是操作系统在窗口 5 秒未取出消息时作出判定、并换成幽灵窗口的机制。本文讲解判定的内部动作、卡死的常见原因、把繁重处理移出 UI 线程的设计,以及挂起的调查步骤。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 和用 CreateThread 自建线程相比,线程池好在哪里?
- 好在大量处理短命工作时的效率,以及线程管理代码的减少。创建和销毁线程的开销不可忽视,因此反复「每来一件工作就 CreateThread、做完就销毁」的应用,或者养着大量只为等待事件而睡眠的线程的应用,换成线程池就能减少线程数和上下文切换。官方文档也把大量并行发出小型工作项的应用、频繁创建短命线程的应用、拥有专门等待内核对象的线程的应用列为线程池的适用对象。反过来,需要改变优先级、需要 COM 的 STA、需要长时间持续运行的专用处理——这些「线程本身要有个性」的工作,仍应像以前一样放在专用线程上。
- 和 QueueUserWorkItem 等老牌线程池函数有什么不同?
- 线程池在 Windows Vista 上被全面重新设计,现在的 threadpoolapiset 系列 API(CreateThreadpoolWork 等)是新 API,QueueUserWorkItem 和 RegisterWaitForSingleObject 等则是旧(遗留)API。新 API 统一了工作线程的种类,可以在一个进程内建立多个互相独立的池,还提供了用清理组批量释放、以及与回调完成联动的释放锁和卸载 DLL 等机制。官方也表示新 API 更简单,在可靠性、性能和灵活性上都更好。旧 API 还有「无法取消已经入队的工作」这类结构性限制,因此新代码请使用新 API。
- 回调里有没有不能做的事?
- 主要有三条。第一,什么都不声明就长时间阻塞或执行长时间的处理。线程池是按「回调会迅速结束」这一前提来调节线程数的,因此耗时较长的处理要么用 CallbackMayRunLong 声明,要么放到专用线程上。第二,同步等待提交到同一个池的另一件工作完成。当所有工作线程都变成「等别的工作线程」时,就会发生池饥饿型死锁。第三,依赖线程的个性。工作线程会在多个回调之间共享,因此改了线程优先级或 COM 初始化状态就直接返回、把状态留在 TLS 里,都会污染下一次回调。至于结束时的收尾(释放锁、卸载 DLL),有 LeaveCriticalSectionWhenCallbackReturns 和 FreeLibraryWhenCallbackReturns 这样的专用机制。
- 在 DLL 里使用线程池,有哪些需要注意的地方?
- 最大的危险是「回调还在运行,DLL 却被卸载了」。卸载之后再执行回调就会触发访问违例。DLL 一方必须在关闭处理中,用 WaitForThreadpoolWorkCallbacks 之类的等待函数(或者清理组的 CloseThreadpoolCleanupGroupMembers)可靠地等到自己发出的回调全部完成,然后再关闭对象。不过在 DllMain 里做这个等待会因为和加载器锁纠缠而死锁,所以原则上要放在显式的关闭函数里,而不是 DllMain 里。如果希望由回调自己在「这是最后一件工作」的场合释放 DLL,还准备了 FreeLibraryWhenCallbackReturns 这个专用 API。
- 已经有 C++ 的 std::async 和 .NET 的 ThreadPool 了,还有直接使用这套 API 的场合吗?
- 有。判断标准是「那一层的工具够不够用」。在 C++ 里如果 std::async 或 std::thread 的粒度就够用,从可移植性看标准库也是第一候选。另一方面,希望把定时器、内核对象等待、异步 I/O 完成统一到同一套回调机制里,希望按工作种类拆分线程池并控制线程数,不希望在 DLL 或 COM 组件里自持线程——这些需求正是 Win32 线程池的覆盖范围。它与 .NET 的 ThreadPool 和 IOCP 的关系,相关文章里已有说明;只要还在写原生代码,了解底下这一层的机制就不会白费。