{"id":111950,"date":"2025-12-31T07:00:00","date_gmt":"2025-12-31T15:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=111950"},"modified":"2025-12-31T07:14:45","modified_gmt":"2025-12-31T15:14:45","slug":"20251231-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20251231-00\/?p=111950","title":{"rendered":"Understanding and mitigating a stack overflow in our task sequencer"},"content":{"rendered":"<p>A customer was using <a title=\"Fixing exception safety in our task_sequencer\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250328-00\/?p=111016\"> the v2 <code>task_sequencer<\/code> class<\/a> we developed some time ago. (Here&#8217;s the <a title=\"Serializing asynchronous operations in C++\/WinRT\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220915-00\/?p=107182\"> v1 task sequencer<\/a>.) They found that they occasionally suffered from stack overflow crashes.<\/p>\n<pre>QueueTaskAsync::&lt;lambda_2&gt;::operator()+0x714\r\nstd::coroutine_handle&lt;void&gt;::resume+0x6c\r\ntask_sequencer::chained_task::complete+0x88\r\ntask_sequencer::completer::~completer+0x58\r\nQueueTaskAsync::&lt;lambda_2&gt;::operator()+0xaf8\r\nstd::coroutine_handle&lt;void&gt;::resume+0x6c\r\ntask_sequencer::chained_task::complete+0x88\r\ntask_sequencer::completer::~completer+0x58\r\nQueueTaskAsync::&lt;lambda_2&gt;::operator()+0xaf8\r\nstd::coroutine_handle&lt;void&gt;::resume+0x6c\r\ntask_sequencer::chained_task::complete+0x88\r\ntask_sequencer::completer::~completer+0x58\r\nQueueTaskAsync::&lt;lambda_2&gt;::operator()+0xaf8\r\nstd::coroutine_handle&lt;void&gt;::resume+0x6c\r\ntask_sequencer::chained_task::complete+0x88\r\ntask_sequencer::completer::~completer+0x58\r\nQueueTaskAsync::&lt;lambda_2&gt;::operator()+0xaf8\r\nstd::coroutine_handle&lt;void&gt;::resume+0x6c\r\ntask_sequencer::chained_task::complete+0x88\r\ntask_sequencer::completer::~completer+0x58\r\n...\r\n<\/pre>\n<p>Reading from the bottom up (to see the sequence chronologically), a coroutine completed, so we resumed the lambda coroutine inside <code>Queue\u00adTask\u00adAsync<\/code>:<\/p>\n<pre>        auto task = [](auto&amp;&amp; current, auto&amp;&amp; makerParam,\r\n                       auto&amp;&amp; contextParam, auto&amp; suspend)\r\n                    -&gt; Async\r\n        {\r\n            completer completer{ std::move(current) };\r\n            auto maker = std::move(makerParam);\r\n            auto context = std::move(contextParam);\r\n\r\n            co_await suspend;\r\n            co_await context;\r\n            co_return co_await maker();\r\n        }(current, std::forward&lt;Maker&gt;(maker),\r\n          winrt::apartment_context(), suspend);\r\n<\/pre>\n<p>When one completer destructs, it resumes the <code>co_await suspend<\/code> in this lambda. The lambda then switches to the correct thread (which we don&#8217;t see in the stack because we are already on the correct thread), asks the maker to start the next coroutine (which we don&#8217;t see on the stack because it returned), and then awaits that coroutine. We don&#8217;t see that coroutine on the stack, which means that it completed synchronously. And then that&#8217;s the end of the lambda, so we start the next one.<\/p>\n<p>Therefore, we run into this problem if there is a sequence of queued tasks, where all those tasks complete synchronously.<\/p>\n<p>So what can we do about it?<\/p>\n<p>We could force the stack to unwind by throwing a <code>co_await winrt::<wbr \/>resume_<wbr \/>background()<\/code> into the lambda after the <code>co_await suspend<\/code>, so that the coroutine resumes on a background thread&#8217;s fresh stack, releasing the thread it was resumed on so it can unwind.<\/p>\n<p>This does soak up a threadpool thread in the case that the <code>apartment_<wbr \/>context()<\/code> is a single-threaded apartment, because the <code>IContext\u00adCallback<\/code> blocks the calling thread while the callback is running. Most people don&#8217;t worry about this problem, but I do because I&#8217;ve had to debug deadlocks that trace back to threadpool exhaustion because all the threads are just waiting for another thread to be ready or finish doing something.<\/p>\n<p>The customer noted that the <code>task_<wbr \/>sequencer<\/code> is always used from the same thread, which happens to be a UI thread. So we can give the task sequencer a <code>Dispatcher\u00adQueue<\/code> that it can use to get back to the UI thread asynchronously via <code>Try\u00adEnqueue()<\/code>.<\/p>\n<pre>struct task_sequencer\r\n{\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">task_sequencer(winrt::Dispatcher\u00adQueue const&amp; queue = nullptr)<\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">    : m_queue(queue) {}                                      <\/span>\r\n    task_sequencer(const task_sequencer&amp;) = delete;\r\n    void operator=(const task_sequencer&amp;) = delete;\r\n\r\nprivate:\r\n    using coro_handle = std::experimental::coroutine_handle&lt;&gt;;\r\n\r\n    struct suspender\r\n    {\r\n        bool await_ready() const noexcept { return false; }\r\n        void await_suspend(coro_handle h)\r\n            noexcept { handle = h; }\r\n        void await_resume() const noexcept { }\r\n\r\n        coro_handle handle;\r\n    };\r\n\r\n    static void* completed()\r\n    { return reinterpret_cast&lt;void*&gt;(1); }\r\n\r\n    struct chained_task\r\n    {\r\n        chained_task(void* state = nullptr) : next(state) {}\r\n\r\n        void continue_with(coro_handle h) {\r\n            if (next.exchange(h.address(),\r\n                        std::memory_order_acquire) != nullptr) {\r\n                h();\r\n            }\r\n        }\r\n\r\n        void complete() {\r\n            auto resume = next.exchange(completed());\r\n            if (resume) {\r\n                coro_handle::from_address(resume).resume();\r\n            }\r\n        }\r\n\r\n        std::atomic&lt;void*&gt; next;\r\n    };\r\n\r\n    struct completer\r\n    {\r\n        ~completer()\r\n        {\r\n            chain-&gt;complete();\r\n        }\r\n        std::shared_ptr&lt;chained_task&gt; chain;\r\n    };\r\n\r\n    winrt::slim_mutex m_mutex;\r\n    std::shared_ptr&lt;chained_task&gt; m_latest =\r\n        std::make_shared&lt;chained_task&gt;(completed());\r\n\r\npublic:\r\n    template&lt;typename Maker&gt;\r\n    auto QueueTaskAsync(Maker&amp;&amp; maker) -&gt;decltype(maker())\r\n    {\r\n        auto node = std::make_shared&lt;chained_task&gt;();\r\n\r\n        suspender suspend;\r\n\r\n        using Async = decltype(maker());\r\n        auto task = [&amp;]() -&gt; Async\r\n        {\r\n            completer completer{ current };\r\n            auto local_maker = std::forward&lt;Maker&gt;(maker);\r\n            <span style=\"border: solid 1px currentcolor;\">auto local_queue = m_queue;<\/span>\r\n\r\n            co_await suspend;\r\n            <span style=\"border: solid 1px currentcolor; border-bottom: none;\">if (m_queue == nullptr) {                          <\/span>\r\n            <span style=\"border: 1px currentcolor; border-style: none solid;\">    co_await winrt::resume_background();           <\/span>\r\n            <span style=\"border: 1px currentcolor; border-style: none solid;\">} else {                                           <\/span>\r\n            <span style=\"border: 1px currentcolor; border-style: none solid;\">    co_await winrt::resume_foreground(local_queue);<\/span>\r\n            <span style=\"border: solid 1px currentcolor; border-top: none;\">}                                                  <\/span>\r\n            co_return co_await local_maker();\r\n        }();\r\n\r\n        {\r\n            winrt::slim_lock_guard guard(m_mutex);\r\n            m_latest.swap(node);\r\n        }\r\n\r\n        node-&gt;continue_with(suspend.handle);\r\n\r\n        return task;\r\n    }\r\n};\r\n<\/pre>\n<p>You provide a <code>Dispatcher\u00adQueue<\/code> when you create the <code>task_<wbr \/>sequencer<\/code>, so that the task sequencer knows which thread the tasks should be started on. if you pass <code>nullptr<\/code> (or don&#8217;t bother to provide a parameter at all), then they start on a background thread. Otherwise, they start on the thread corresponding to the dispatcher queue.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The recurring problem of synchronous resumption.<\/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-111950","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>The recurring problem of synchronous resumption.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111950","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=111950"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111950\/revisions"}],"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=111950"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=111950"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=111950"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}