{"id":112417,"date":"2026-06-12T07:00:00","date_gmt":"2026-06-12T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=112417"},"modified":"2026-06-13T22:56:52","modified_gmt":"2026-06-14T05:56:52","slug":"20260612-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260612-00\/?p=112417","title":{"rendered":"How can I schedule work on a thread pool with low latency?"},"content":{"rendered":"<p>A customer had a callback that was used to report data being produced by a hardware device. The rule for the callback is that it has to return quickly so that the code wouldn&#8217;t miss the next batch of data because the device itself has a very small buffer: If they spend too much time in the callback, the buffer will overflow and data will be lost.<\/p>\n<p>To avoid clogging the receiving thread, the customer queued a work item to the thread pool to process the data that was just received. However, they found that sometimes, the work item doesn&#8217;t run immediately but rather has a 100ms latency. But their program needs to process the data within 20ms. Is there a way to set a deadline on a thread pool work item, so that the system will make sure that it runs before a certain period of time elapses?<\/p>\n<p>As I&#8217;ve noted before, <a title=\"Trying to make the thread pool more responsive to a large queue of long-running work items\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20170623-00\/?p=96455\"> the thread pool is designed for throughput, not latency<\/a>. There is no option to set a deadline on a work item.<\/p>\n<p>One reason why the thread pool is being slow to dispatch the work items is that there are other unrelated work items in the thread pool, and those other tasks are competing with your data processing task for the thread pool&#8217;s attention. On top of that, some of those other tasks might be long-running, which takes a thread pool thread out of commission for an extended period.<\/p>\n<p>You can take these conflicting work items out of the picture by creating your own custom thread pool: Call <code>Create\u00adThread\u00adPool<\/code> and queue your work to that thread pool (by setting that thread pool in the work item&#8217;s environment). Now you won&#8217;t have any competing work items getting in front of you in the thread pool work queue because those competing work items are going to the process default thread pool and not to your private thread pool.<\/p>\n<p>Note however that even though your work items are no longer fighting with other work items for the attention of your private thread pool, those other work items are still running on the process default thread pool, so they are still competing against your work items for CPU. But at least your work item got dispatched.<\/p>\n<p>I&#8217;m guessing that the order in which the batches are processed is important, so you should set your private thread pool&#8217;s maximum thread count to 1 so that you don&#8217;t start processing one batch of data until you finish processing the previous batch. This effectively serializes the work items, but that&#8217;s what you want if you intend to process the batches in order.<\/p>\n<p>In the case where you have a single-minded thread pool, you can prepare everything ahead of time so that all you have to do in the callback itself is call <code>SubmitThreadpoolWork<\/code> on a pre-created reusable work item.<\/p>\n<pre>\/\/ One-time preparation\r\npool = CreateThreadpool();\r\nif (!pool) \u27e6 error \u27e7\r\n\r\nTP_CALLBACK_ENVIRON env;\r\nInitializeThreadpoolEnvironment(&amp;env);\r\nSetThreadpoolCallbackPool(&amp;env, pool);\r\nwork = CreateThreadpoolWork(ProcessData, nullptr, &amp;env);\r\nif (!work) \u27e6 error \u27e7\r\n\r\nvoid Callback()\r\n{\r\n    \u27e6 add data to data queue \u27e7\r\n    SubmitThreadpoolWork(work); \/\/ request another callback\r\n}\r\n<\/pre>\n<p>If you step back and look at this, you might realize that all we did was create a worker thread, but one where we delegated all the bookkeeping to the thread pool. Also, this particular customer was writing code in C#, and the BCL doesn&#8217;t have built-in support for custom thread pools.<\/p>\n<p>So if all we have is a worker thread, maybe we can just make a worker thread. Here&#8217;s a really simple one.<\/p>\n<pre>Queue&lt;Data&gt; queue = new Queue&lt;Data&gt;();\r\n\r\nData WaitForWork()\r\n{\r\n    while (true) {\r\n        lock (queue) {\r\n            if (queue.Count &gt; 0) {\r\n                return queue.Dequeue();\r\n            }\r\n            Monitor.Wait(queue);\r\n        }\r\n    }\r\n}\r\n\r\nvoid WorkerThread()\r\n{\r\n    Data data;\r\n    while ((data = WaitForWork()) != null) {\r\n        \u27e6 process the data #&amp;x27e7;\r\n    }\r\n}\r\n\r\nvoid QueueWork(Data data)\r\n{\r\n    lock (queue) {\r\n        queue.Enqueue(data);\r\n        Monitor.Pulse(queue);\r\n    }\r\n}\r\n\r\nvoid EndWork()\r\n{\r\n    QueueWork(null);\r\n}\r\n<\/pre>\n<p>The worker thread waits for elements to show up in the queue, and once one appears, it dequeues it and does whatever processing you want. If the queued value is <code>null<\/code>, that means that the worker thread is no longer needed, and it exits.<\/p>\n<p>You can do something similar in C++ with a <code>std::<wbr \/>queue<\/code> and a condition variable.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The thread pool is designed for throughput, not latency.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-112417","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>The thread pool is designed for throughput, not latency.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112417","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=112417"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112417\/revisions"}],"predecessor-version":[{"id":112418,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112417\/revisions\/112418"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=112417"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=112417"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=112417"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}