{"id":105669,"date":"2021-09-10T07:00:00","date_gmt":"2021-09-10T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=105669"},"modified":"2021-09-10T06:49:08","modified_gmt":"2021-09-10T13:49:08","slug":"20210910-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20210910-00\/?p=105669","title":{"rendered":"Ordering asynchronous updates with coroutines, part 5: Bowing out via cancellation"},"content":{"rendered":"<p>Last time, we showed how a coroutine could check after every <code>co_await<\/code> whether its work has been superseded, in which case it just gives up rather than proceeding with a calculation whose result won&#8217;t be used anyway.<\/p>\n<p>We noted that a coroutine provider can <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20210428-00\/?p=105160\"> snoop on every <code>co_await<\/code> in the coroutine body<\/a> by means of the <code>await_transform<\/code> method. C++\/WinRT uses this feature to implement a few things. One of them is making every <code>co_await<\/code> check whether the coroutine has been cancelled and throwing <code>hresult_canceled<\/code> if so. We can take advantage of this by using cancellation to stop any existing instance of the coroutine.<\/p>\n<pre>winrt::IAsyncAction Widget::RecalcWorkerAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n    auto cancellation = co_await winrt::get_cancellation_token();\r\n\r\n    winrt::hstring messageId;\r\n    winrt::hstring lang;\r\n    {\r\n        std::lock_guard guard{ m_mutex };\r\n        messageId = m_messageId;\r\n        lang = m_lang;\r\n    }\r\n\r\n    auto resolved = co_await ResolveLanguageAsync(lang);\r\n    auto library = co_await GetResourceLibraryAsync(resolved);\r\n    auto message = library.LookupResourceAsync(messageId);\r\n\r\n    std::lock_guard guard{ m_mutex };\r\n    if (!cancellation()) {\r\n        m_message = message;\r\n    }\r\n}\r\n<\/pre>\n<p>We move the recalculation into a worker function and rely on cancellation to tell us when to stop calculating. The C++\/WinRT library automatically checks for cancellation at each <code>co_await<\/code> so the only explicit check we need is the final one.<\/p>\n<p>The <code>IAsyncAction<\/code> produced by <code>RecalcWorkerAsync<\/code> is managed by the <code>RecalcAsync<\/code> function:<\/p>\n<pre>winrt::IAsyncAction m_pendingAction;\r\n\r\nwinrt::IAsyncAction Widget::SetPendingAction(\r\n    winrt::IAsyncAction const&amp; action)\r\n{\r\n    winrt::IAsyncAction pendingAction;\r\n    {\r\n        std::lock_guard guard{ m_mutex };\r\n        pendingAction = std::exchange(m_pendingAction, nullptr);\r\n    }\r\n    if (pendingAction) {\r\n        pendingAction.Cancel();\r\n    }\r\n}\r\n\r\nwinrt::IAsyncAction Widget::RecalcAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n\r\n    SetPendingAction(nullptr);\r\n\r\n    auto currentAction = RecalcWorkerAsync();\r\n\r\n    SetPendingAction(currentAction);\r\n\r\n    try {\r\n        co_await currentAction;\r\n    } catch (winrt::hresult_canceled const&amp;) {\r\n        \/\/ ignore cancellation\r\n    }\r\n}\r\n<\/pre>\n<p>There are a few steps here.<\/p>\n<p>Before we start, we cancel the previous operation. The call to <code>Cancel<\/code> must happen outside the lock, because the coroutine completion function is invoked synchronously from inside the <code>Cancel<\/code>, and we don&#8217;t want to let foreign code run while inside our lock.<\/p>\n<p>Next, we start the new operation.<\/p>\n<p>And then we make the new operation become the current operation. There is a race here where two threads both start a new operation at the same time, in which case we have to cancel any possible interloper.<\/p>\n<p>The need for the early cancel stems from this race condition that can occur if we remove the first call to <code>Set\u00adPending\u00adAction<\/code>:<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse;\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<th style=\"border: 1px black; border-style: none solid solid solid;\">Thread 1<\/th>\n<th style=\"border: 1px black; border-style: none solid solid solid;\">Thread 2<\/th>\n<\/tr>\n<tr>\n<td style=\"border: 1px black; border-style: none solid;\"><code>RecalcWorkerAsync<\/code>:<br \/>\n<code>\u00a0currentAction = RecalcAsync()<\/code><br \/>\n<code>\u00a0enter lock<\/code><br \/>\n<code>\u00a0std::exchange(<br \/>\n              \u00a0\u00a0m_pendingAction, currentAction)<\/code><br \/>\n<code>\u00a0exit lock<\/code><br \/>\n<code>RecalcAsync<\/code>:<br \/>\n<code>\u00a0co_await ResolveLanguageAsync(...);<\/code><br \/>\n<code>\u00a0co_await GetResourceLibraryAsync(...);<\/code><br \/>\n<code>\u00a0co_await LookupResourceAsync(...);<\/code><\/td>\n<td style=\"border: 1px black; border-style: none solid;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: 1px black; border-style: none solid;\">\u00a0<\/td>\n<td style=\"border: 1px black; border-style: none solid;\"><code>RecalcWorkerAsync<\/code>:<br \/>\n<code>\u00a0currentAction = RecalcAsync()<\/code><br \/>\n<code>\u00a0enter lock<\/code><br \/>\n<code>\u00a0pendingAction = std::exchange(<br \/>\n              \u00a0\u00a0m_pendingAction, currentAction)<\/code><br \/>\n<code>\u00a0exit lock<\/code><br \/>\n<code>RecalcAsync<\/code>:<br \/>\n<code>\u00a0co_await ResolveLanguageAsync(...);<\/code><br \/>\n<code>\u00a0co_await GetResourceLibraryAsync(...);<\/code><br \/>\n<code>\u00a0co_await LookupResourceAsync(...);<\/code><br \/>\n<code>\u00a0enter lock<\/code><br \/>\n<code>\u00a0verify not cancelled<\/code><br \/>\n<code>\u00a0m_message = message<\/code><br \/>\n<code>\u00a0exit lock<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"border: 1px black; border-style: none solid;\"><code>\u00a0enter lock<\/code><br \/>\n<code>\u00a0verify not cancelled<\/code><br \/>\n<code>\u00a0m_message = message<\/code><br \/>\n<code>\u00a0exit lock<\/code><\/td>\n<td style=\"border: 1px black; border-style: none solid;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: 1px black; border-style: none solid;\">\u00a0<\/td>\n<td style=\"border: 1px black; border-style: none solid;\"><code>RecalcWorkerAsync<\/code> continues:<br \/>\n<code>\u00a0pendingAction.Cancel()<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The cancellation of the previous call to <code>RecalcAsync<\/code> happens too late. The previous recalculation raced against the current recalculation, and the current one happened to finish first, causing the previous one to overwrite the result.<\/p>\n<p>If the <code>Widget<\/code> is single-threaded, then we can get rid of the locks, and that also removes some of the subtle race conditions.<\/p>\n<pre>winrt::IAsyncAction Widget::RecalcWorkerAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n    auto cancellation = co_await winrt::get_cancellation_token();\r\n\r\n    <span style=\"color: blue;\">auto messageId = m_messageId;\r\n    auto lang = m_lang;<\/span>\r\n\r\n    auto resolved = co_await ResolveLanguageAsync(lang);\r\n    auto library = co_await GetResourceLibraryAsync(resolved);\r\n    auto message = library.LookupResourceAsync(messageId);\r\n\r\n    <span style=\"color: red;\">\/\/ <span style=\"text-decoration: line-through;\">std::lock_guard guard{ m_mutex };<\/span><\/span>\r\n    if (!cancellation()) {\r\n        m_message = message;\r\n    }\r\n}\r\n\r\nwinrt::IAsyncAction Widget::RecalcAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n\r\n    auto currentAction = RecalcWorkerAsync();\r\n\r\n    <span style=\"color: blue;\">auto previousAction = std::exchange(m_pendingAction, currentAction);\r\n    if (previousAction) previousAction.Cancel();<\/span>\r\n\r\n    try {\r\n        co_await currentAction;\r\n    } catch (winrt::hresult_canceled const&amp;) {\r\n        \/\/ ignore cancellation\r\n    }\r\n}\r\n<\/pre>\n<p>The race condition doesn&#8217;t exist because there is no opportunity for the previous action to do any work between the time we start the new task and cancel the old one. The only race is between the final <code>co_await<\/code> and the cancellation, and one final check takes care of that.<\/p>\n<p>One thing you might notice about this pattern is that <code>m_pendingAction<\/code> is never nulled out. It always holds the last successful action, even after it has completed. This means that the coroutine remains allocated, even though has ended its useful life, consuming memory (probably not too much) and keeping its inbound parameters alive (fortunately, we have none). If the coroutine frame is large, or if there are inbound parameters which you need to run down promptly,\u00b9 you can clean it up once you&#8217;ve finished waiting for it.<\/p>\n<pre>winrt::IAsyncAction Widget::RecalcAsync()\r\n{\r\n    auto lifetime = get_strong();\r\n\r\n    auto currentAction = RecalcWorkerAsync();\r\n\r\n    auto previousAction = std::exchange(m_pendingAction, currentAction);\r\n    if (previousAction) previousAction.Cancel();\r\n\r\n    try {\r\n        co_await currentAction;\r\n    } catch (winrt::hresult_canceled const&amp;) {\r\n        \/\/ ignore cancellation\r\n    }\r\n    <span style=\"color: blue;\">if (m_pendingAction == currentAction) {\r\n        m_pendingAction = nullptr;\r\n    }<\/span>\r\n}\r\n<\/pre>\n<p>\u00b9 One example of needing to run down inbound parameters is the case where they belong to another component. You don&#8217;t want to extend the lifetime of foreign objects beyond the end of the useful life of the coroutine. Not only could that create problems with that other component (say, because the other component is single-threaded and the thread that hosts it wants to exit), it could also introduce circular references between that other component and your component.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Using the built-in way of stopping a coroutine.<\/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-105669","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Using the built-in way of stopping a coroutine.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/105669","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=105669"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/105669\/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=105669"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=105669"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=105669"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}