{"id":111016,"date":"2025-03-28T07:00:00","date_gmt":"2025-03-28T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=111016"},"modified":"2025-03-28T06:45:16","modified_gmt":"2025-03-28T13:45:16","slug":"20250328-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250328-00\/?p=111016","title":{"rendered":"Fixing exception safety in our <CODE>task_<WBR>sequencer<\/CODE>"},"content":{"rendered":"<p>Some time ago, we developed a <a title=\"Serializing asynchronous operations in C++\/WinRT\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220915-00\/?p=107182\"> <code>task_<wbr \/>sequencer<\/code> class<\/a> for running asynchronous operations in sequence. There&#8217;s a problem with the implementation of <code>Queue\u00adTask\u00adAsync<\/code>: What happens if an exception occurs?<\/p>\n<p>Let&#8217;s look at the various places an exception can occur in <code>Queue\u00adTask\u00adAsync<\/code>.<\/p>\n<pre>    template&lt;typename Maker&gt;\r\n    auto QueueTaskAsync(Maker&amp;&amp; maker) -&gt;decltype(maker())\r\n    {\r\n        <span style=\"border: solid 1px currentcolor;\">auto current = std::make_shared&lt;chained_task&gt;();<\/span>\r\n        auto previous = [&amp;]\r\n        {\r\n            winrt::slim_lock_guard guard(m_mutex);\r\n            <span style=\"border: solid 1px currentcolor;\">return std::exchange(m_latest, current); \u2190 oops<\/span>\r\n        }();\r\n\r\n        suspender suspend;\r\n\r\n        using Async = decltype(maker());\r\n        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        }<span style=\"border: solid 1px currentcolor;\">(current, std::forward&lt;Maker&gt;(maker),<\/span>\r\n          winrt::apartment_context(), suspend);\r\n\r\n        previous-&gt;continue_with(suspend.handle);\r\n\r\n        return task;\r\n    }\r\n<\/pre>\n<p>If an exception occurs at <code>make_<wbr \/>shared<\/code>, then no harm is done because we haven&#8217;t done anything yet.<\/p>\n<p>If an exception occurs when starting the lambda task, then we are in trouble. We have already linked the <code>current<\/code> onto <code>m_latest<\/code>, but we will never call <code>continue_<wbr \/>with()<\/code>, so the chain of tasks stops making progress.<\/p>\n<p>To fix this, we need to delay hooking up the <code>current<\/code> to the chain of <code>chained_<wbr \/>task<\/code>s until we are sure that we have a task to chain. This means that the <code>std::<wbr \/>exchange(<wbr \/>m_latest, current);<\/code> needs to wait until the task is created. But we can&#8217;t just swap the order of the two operations because the task does a <code>std::move(<wbr \/>current)<\/code>, which empties out the <code>current<\/code><\/p>\n<pre>    template&lt;typename Maker&gt;\r\n    auto QueueTaskAsync(Maker&amp;&amp; maker) -&gt;decltype(maker())\r\n    {\r\n        auto current = 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 = [](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\r\n        <span style=\"border: solid 1px currentcolor; border-bottom: none;\">\/\/ moved this to after we create the task                <\/span>\r\n        <span style=\"border: 1px currentcolor; border-style: none solid;\">auto previous = [&amp;]                                      <\/span>\r\n        <span style=\"border: 1px currentcolor; border-style: none solid;\">{                                                        <\/span>\r\n        <span style=\"border: 1px currentcolor; border-style: none solid;\">    winrt::slim_lock_guard guard(m_mutex);               <\/span>\r\n        <span style=\"border: 1px currentcolor; border-style: none solid;\">    return std::exchange(m_latest, current); \u2190 still oops<\/span>\r\n        <span style=\"border: solid 1px currentcolor; border-top: none;\">}();                                                     <\/span>\r\n\r\n        previous-&gt;continue_with(suspend.handle);\r\n\r\n        return task;\r\n    }\r\n<\/pre>\n<p>The last person to use the <code>current<\/code> is allowed to move from it, so we have to move the move.<\/p>\n<pre>    template&lt;typename Maker&gt;\r\n    auto QueueTaskAsync(Maker&amp;&amp; maker) -&gt;decltype(maker())\r\n    {\r\n        auto current = 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 = [](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{ <span style=\"border: solid 1px currentcolor;\">current<\/span> };\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\r\n        auto previous = [&amp;]\r\n        {\r\n            winrt::slim_lock_guard guard(m_mutex);\r\n            return std::exchange(m_latest, <span style=\"border: solid 1px currentcolor;\">std::move<\/span>(current));\r\n        }();\r\n\r\n        previous-&gt;continue_with(suspend.handle);\r\n\r\n        return task;\r\n    }\r\n<\/pre>\n<p>While we&#8217;re here, we may as well do some cleaning up. For example, there was no need to create the <code>apartment_<wbr \/>context<\/code> outside the lambda and move it into the lambda locals. We can just create it as a lambda local.<\/p>\n<pre>        auto task = [](auto&amp;&amp; current, auto&amp;&amp; makerParam,\r\n                       <span style=\"border: solid 1px currentcolor;\">\/* <span style=\"text-decoration: line-through;\">auto&amp;&amp; contextParam,<\/span> *\/<\/span> auto&amp; suspend)\r\n                    -&gt; Async\r\n        {\r\n            completer completer{ current };\r\n            auto maker = std::move(makerParam);\r\n            auto context = <span style=\"border: solid 1px currentcolor;\">winrt::apartment_context()<\/span>;\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          <span style=\"border: solid 1px currentcolor;\">\/* <span style=\"text-decoration: line-through;\">winrt::apartment_context(),<\/span> *\/<\/span> suspend);\r\n<\/pre>\n<p>And instead of passing the other lambda parameters by reference, we can use a reference capture. We just have to be careful to copy them to locals before our first <code>co_await<\/code>. (And there was another bug in the original version: It moved the <code>maker<\/code> into the local rather than forwarding it.)<\/p>\n<pre>        auto task = [<span style=\"border: solid 1px currentcolor;\">&amp;<\/span>]() -&gt; Async\r\n        {\r\n            completer completer{ current };\r\n            auto <span style=\"border: solid 1px currentcolor;\">local_maker<\/span> = <span style=\"border: solid 1px currentcolor;\">std::forward&lt;Maker&gt;(maker)<\/span>;\r\n            auto context = winrt::apartment_context();\r\n\r\n            co_await suspend;\r\n            co_await context;\r\n            co_return co_await <span style=\"border: solid 1px currentcolor;\">local_maker<\/span>();\r\n        }(<span style=\"border: solid 1px currentcolor;\">\/* <span style=\"text-decoration: line-through;\">\u27e6 ... \u27e7<\/span> *\/<\/span>);\r\n<\/pre>\n<p>And now that the last consumer of <code>current<\/code> is the <code>Queue\u00adTask\u00adAsync<\/code> function itself, we can swap it instead of moving it out of one variable and into another.<\/p>\n<pre>        <span style=\"border: solid 1px currentcolor;\">\/* <span style=\"text-decoration: line-through;\">auto previous = [&amp;]<\/span> *\/<\/span>\r\n        {\r\n            winrt::slim_lock_guard guard(m_mutex);\r\n            <span style=\"border: solid 1px currentcolor;\">m_latest.swap(current);<\/span>\r\n        }<span style=\"border: solid 1px currentcolor;\">\/* <span style=\"text-decoration: line-through;\">()<\/span> *\/<\/span>\r\n\r\n        <span style=\"border: solid 1px currentcolor;\">current<\/span>-&gt;continue_with(suspend.handle);\r\n<\/pre>\n<p>The name <code>current<\/code> is confusing for a variable that actually holds the <code>previous<\/code> entry, so let&#8217;s give it a generic name <code>node<\/code>.<\/p>\n<p>The result of all this cleanup is the following class. Everything outside <code>Queue\u00adTask\u00adAsync<\/code> is unchanged, but I include it here for copy-paste-friendliness.<\/p>\n<pre>struct task_sequencer\r\n{\r\n    task_sequencer() = default;\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            auto context = winrt::apartment_context();\r\n\r\n            co_await suspend;\r\n            co_await context;\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","protected":false},"excerpt":{"rendered":"<p>Exception safety, the forgotten requirement.<\/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-111016","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Exception safety, the forgotten requirement.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111016","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=111016"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111016\/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=111016"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=111016"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=111016"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}