如何在低延迟的线程池上调度工作?
摘要
本文来自《The Old New Thing》,解释了Windows线程池是为吞吐量而非延迟优化的,并提供了低延迟调度的解决方案,例如创建自定义线程池或使用专用工作线程,并附有C++和C#的代码示例。
<p>一位客户有一个用于报告硬件设备生成数据的回调函数。该回调的规则是必须快速返回,以便代码不会错过下一批数据,因为设备本身的缓冲区很小:如果在回调中花费太多时间,缓冲区将溢出,数据将丢失。</p>
<p>为了避免阻塞接收线程,客户将一个工作项排队到线程池以处理刚接收到的数据。然而,他们发现有时工作项不会立即运行,而是有100毫秒的延迟。但他们的程序需要在20毫秒内处理数据。有没有办法在线程池工作项上设置截止时间,以便系统确保在一定时间内运行它?</p>
<p>正如我之前提到的,<a title="尝试让线程池对大量长时间运行的工作项更敏感" href="https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455">线程池是为吞吐量而非延迟设计的</a>。没有选项可以为工作项设置截止时间。</p>
<p>线程池调度工作项缓慢的原因之一是线程池中存在其他不相关的工作项,这些其他任务与您的数据处理任务竞争线程池的注意力。此外,其中一些其他任务可能运行时间较长,这会使线程池线程在较长时间内不可用。</p>
<p>您可以通过创建自己的自定义线程池来消除这些冲突的工作项:调用<code>CreateThreadPool</code>并将您的工作排队到该线程池(通过在工作项的环境中设置该线程池)。现在,您将不会有任何竞争的工作项在线程池工作队列中排在您前面,因为这些竞争的工作项将进入进程默认线程池,而不是您的私有线程池。</p>
<p>但请注意,尽管您的工作项不再与其他工作项争夺您的私有线程池的注意力,但这些其他工作项仍在进程默认线程池上运行,因此它们仍在与您的工作项竞争CPU。但至少您的工作项被调度了。</p>
<p>我猜测批处理的顺序很重要,因此您应该将私有线程池的最大线程数设置为1,这样直到处理完前一批数据后才开始处理下一批数据。这实际上串行化了工作项,但如果您打算按顺序处理批次,这正是您想要的。</p>
<p>在您有一个专注的线程池的情况下,您可以提前准备好所有内容,这样在回调本身中您只需在预创建的可重用工作项上调用<code>SubmitThreadpoolWork</code>。</p>
<pre>// One-time preparation
pool = CreateThreadpool();
if (!pool) ⟦ error ⟧
TP_CALLBACK_ENVIRON env;
InitializeThreadpoolEnvironment(&env);
SetThreadpoolCallbackPool(&env, pool);
work = CreateThreadpoolWork(ProcessData, nullptr, &env);
if (!work) ⟦ error ⟧
void Callback()
{
⟦ add data to data queue ⟧
SubmitThreadpoolWork(work); // request another callback
}
</pre>
<p>如果您退一步来看,您可能会意识到我们所做的只是创建一个工作线程,但将所有簿记工作委托给了线程池。此外,这位特定客户正在用C#编写代码,而BCL没有内置支持自定义线程池。</p>
<p>因此,如果我们只有工作线程,也许我们可以直接创建一个工作线程。下面是一个非常简单的例子。</p>
<pre>Queue<Data> queue = new Queue<Data>();
Data WaitForWork()
{
while (true) {
lock (queue) {
if (queue.Count > 0) {
return queue.Dequeue();
}
Monitor.Wait(queue);
}
}
}
void WorkerThread()
{
Data data;
while ((data = WaitForWork()) != null) {
⟦ process the data #&x27e7;
}
}
void QueueWork(Data data)
{
lock (queue) {
queue.Enqueue(data);
Monitor.Pulse(queue);
}
}
void EndWork()
{
QueueWork(null);
}
</pre>
<p>工作线程等待队列中出现元素,一旦出现,就将其出队并执行所需的任何处理。如果排队值为null,则表示不再需要该工作线程,它退出。</p>
<p>您可以在C++中使用<code>std::<wbr />queue</code>和条件变量执行类似操作。</p>
<p>本文<a href="https://devblogs.microsoft.com/oldnewthing/20260612-00/?p=112417">《如何在低延迟的线程池上调度工作?》</a>首次出现在<a href="https://devblogs.microsoft.com/oldnewthing">《The Old New Thing》</a>上。</p>
查看缓存全文
缓存时间: 2026/06/15 00:56
# 如何在延迟敏感的情况下在线程池中安排工作? - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260612-00?p=112417
有位客户有一个回调函数,用于上报硬件设备产生的数据。回调函数的规则是必须快速返回,以免因设备自身缓冲区过小而错过下一批数据:如果在回调中花费太多时间,缓冲区就会溢出,数据就会丢失。为了避免阻塞接收线程,客户将工作项排队到线程池中,以处理刚刚接收到的数据。然而,他们发现有时工作项并不会立即执行,而是存在 100ms 的延迟。但他们的程序需要在 20ms 内处理完数据。有没有办法为线程池工作项设置一个截止时间,让系统确保它在一定时间内运行?
正如我之前提到的,[线程池是为吞吐量而非低延迟设计的](https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455)。没有为工作项设置截止时间的选项。线程池调度工作项缓慢的一个原因是,线程池中还有其他不相关的工作项,这些任务与你的数据处理任务竞争线程池的注意力。此外,其中一些任务可能长时间运行,导致线程池线程长时间无法使用。
你可以通过创建自己的自定义线程池来消除这些冲突工作项的影响:调用 `CreateThreadpool` 并将你的工作排队到该线程池(通过在工作项环境中设置该线程池)。这样,就不会有其他工作项在你的线程池工作队列中插队,因为这些冲突工作项会进入进程默认线程池,而不是你的私有线程池。
但请注意,即使你的工作项不再与其他工作项争夺私有线程池的注意力,那些其他工作项仍然在进程默认线程池上运行,因此它们仍然会与你的工作项竞争 CPU。但至少你的工作项会被调度。
我猜测批处理数据的顺序很重要,因此你应该将私有线程池的最大线程数设置为 1,这样只有处理完前一批数据后才会开始处理下一批。这实际上是将工作项串行化,但如果你打算按顺序处理批次,这正是你想要的。
对于这种单一用途的线程池,你可以提前准备好所有内容,这样在回调函数中只需对预先创建的可重用工作项调用 `SubmitThreadpoolWork` 即可。
`` ``
// 一次性准备
pool = CreateThreadpool();
if (!pool) ⟦ 错误处理 ⟧
TP_CALLBACK_ENVIRON env;
InitializeThreadpoolEnvironment(&env);
SetThreadpoolCallbackPool(&env, pool);
work = CreateThreadpoolWork(ProcessData, nullptr, &env);
if (!work) ⟦ 错误处理 ⟧
void Callback()
{
⟦ 将数据添加到数据队列 ⟧
SubmitThreadpoolWork(work); // 请求另一个回调
}
`` ``
如果你退一步来看,可能会意识到我们只是创建了一个工作线程,只不过将所有簿记工作委托给了线程池。此外,这位客户是用 C# 编写的代码,而 BCL 本身并不支持自定义线程池。所以如果最终只是需要一个工作线程,也许我们直接创建一个工作线程就好。下面是一个非常简单的示例。
`` ``
Queue queue = new Queue();
Data WaitForWork()
{
while (true)
{
lock (queue)
{
if (queue.Count > 0)
return queue.Dequeue();
Monitor.Wait(queue);
}
}
}
void WorkerThread()
{
Data data;
while ((data = WaitForWork()) != null)
{
⟦ 处理数据 #x27e7; ⟧
}
}
void QueueWork(Data data)
{
lock (queue)
{
queue.Enqueue(data);
Monitor.Pulse(queue);
}
}
void EndWork()
{
QueueWork(null);
}
`` ``
工作线程等待队列中出现元素,一旦有元素,就将其出队并执行你想要的任何处理。如果入队的值为 `null`,则表示不再需要工作线程,它就会退出。在 C++ 中,你可以用 `std::queue` 和条件变量实现类似的功能。
### 分类
### 话题
## 作者
Raymond Chen
Raymond 参与 Windows 的发展已有 30 多年。2003 年,他创办了一个名为 The Old New Thing 的网站,其受欢迎程度远超他最疯狂的想象——这一发展至今仍让他感到不安。该网站后来出版了一本书,巧合的是书名也是 *The Old New Thing*(Addison Wesley,2007 年)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些不包含任何有用信息的故事。
相似文章
为何低延迟Java仍需严谨的编码纪律?
讨论为何在现代JVM优化下,低延迟Java仍需严谨的编码实践。
构建低延迟和高吞吐量AI代理的经验教训
本文分享了构建低延迟、高吞吐量AI代理的实用经验,包括工作负载估算、令牌减少、并行处理、微服务以及处理LLM故障等。
Silk: 开源协作式纤程调度器
Silk 是一个面向 Linux 的开源协作式纤程调度器,具有每 CPU 调度线程、io_uring 集成和拓扑感知的工作窃取功能,专为低开销下的高并发而设计。
面向LLM赋能代理工作流的可靠设计:优化延迟-可靠性-成本权衡
本文分析了LLM赋能代理工作流中延迟、可靠性和成本之间的权衡,引入了性能模型,并推导出了如注水令牌分配等最优资源分配策略。
慢速软件:为高延迟系统开发辩护
文章认为,AI编程加速了开发速度与系统重要性的脱钩,导致关键系统变得脆弱且故障波及范围广,并倡导实施强制谨慎设计的‘慢速软件’。