{"id":106602,"date":"2022-05-06T07:00:00","date_gmt":"2022-05-06T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=106602"},"modified":"2022-05-06T10:53:38","modified_gmt":"2022-05-06T17:53:38","slug":"20220506-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220506-00\/?p=106602","title":{"rendered":"On awaiting a task with a timeout in C++\/WinRT"},"content":{"rendered":"<p>Last time, we studied <a title=\"On awaiting a task with a timeout in C#\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220505-00\/?p=106585\"> ways of awaiting a task with a timeout in C#<\/a>. Now we&#8217;ll apply what we learned to C++\/WinRT.<\/p>\n<p>C++\/WinRT already has a <code>when_any<\/code> function which completes as soon as any of the provided coroutines completes, so we can follow a similar pattern. An added wrinkle is that <code>winrt::<wbr \/>resume_after<\/code> does not return an <code>IAsyncAction<\/code> so we&#8217;ll have to adapt it. This isn&#8217;t too much an extra wrinkle, since we ended up having to adapt <code>Task.Delay<\/code> in C# anyway, so it&#8217;s work that gets done sooner or later. It&#8217;s just that in C++\/WinRT, it&#8217;s done sooner.<\/p>\n<pre>co_await winrt::when_any(\r\n    DoSomethingAsync(),\r\n    [] -&gt; IAsyncAction { co_await winrt::resume_after(1s); });\r\n<\/pre>\n<p>The C++\/WinRT <code>when_<wbr \/>any<\/code> doesn&#8217;t tell you who the winner was. It just completes with the result of the task that completed first. This means that we don&#8217;t know whether the <code>when_<wbr \/>any<\/code> finished due to normal completion or timeout.<\/p>\n<p>We could infer which one completed first by having the timeout set a flag.<\/p>\n<pre>auto timed_out = make_shared(false);\r\n\r\nco_await winrt::when_any(\r\n    DoSomethingAsync(),\r\n    [](auto flag) -&gt; IAsyncAction\r\n        { co_await winrt::resume_after(1s); *flag = true; }(timed_out));\r\nif (timed_out) { ... }\r\n<\/pre>\n<p>For <code>IAsync\u00adOperation<\/code>, we need our timer to return some fallback value:<\/p>\n<pre>template&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync delayed_async_result(\r\n    TimeSpan delay)\r\n{\r\n    co_await winrt::resume_after(delay);\r\n}\r\n\r\ntemplate&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;!std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync delayed_async_result(\r\n    TimeSpan delay,\r\n    Result fallback = winrt_empty_value&lt;Result&gt;())\r\n{\r\n    co_await winrt::resume_after(delay);\r\n    co_return fallback;\r\n}\r\n<\/pre>\n<p>The <code>delayed_<wbr \/>async_<wbr \/>result<\/code> is cumbersome for multiple reasons.<\/p>\n<p>First, there&#8217;s the need to identify the result of the <code>Async<\/code> so we can generate a default fallback value if necessary. To do that, we infer it from the value returned by <code>GetResults()<\/code>. This covers <code>IAsync\u00adAction<\/code>, <code>IAsync\u00adAction\u00adWith\u00adProgress<\/code>, <code>IAsync\u00adOperation<\/code>, and <code>IAsync\u00adOperation\u00adWith\u00adProgress<\/code>.<\/p>\n<p>If that result is <code>void<\/code>, then we need to remove the extra <code>Result<\/code> parameter.<\/p>\n<p>If that result is not <code>void<\/code>, we need to come up with the default fallback value. This is tricky for the case of Windows Runtime classes, but we dealt with that a little while ago when we wrote the <code>winrt_<wbr \/>empty_<wbr \/>value<\/code> function.<\/p>\n<p>Or maybe we want to raise a timeout exception if the operation times out:<\/p>\n<pre>template&lt;typename Async&gt;\r\nAsync delayed_timeout_exception(TimeSpan delay)\r\n{\r\n    co_await winrt::resume_after(delay);\r\n    throw winrt::hresult_error(HRESULT_FROM_WIN32(ERROR_TIMEOUT));\r\n}\r\n<\/pre>\n<p>We can use these in conjunction with <code>when_any<\/code> to await a Windows Runtime asynchronous activity with a timeout.<\/p>\n<p>It could be an <code>IAsync\u00adAction<\/code> or <code>IAsync\u00adAction\u00adWith\u00adProgress<\/code>, in which case the <code>co_await<\/code> simple returns early.<\/p>\n<pre>auto somethingTask = DoSomethingAsync();\r\nco_await winrt::when_any(\r\n    somethingTask,\r\n    delayed_async_result&lt;decltype(somethingTask)&gt;(1s));\r\n<\/pre>\n<p>Or it could be an <code>IAsync\u00adOperation<\/code> or <code>IAsync\u00adOperation\u00adWith\u00adProgress<\/code>, in which case the <code>co_await<\/code> produces the result or the fallback value:<\/p>\n<pre>auto somethingTask = GetSomethingAsync();\r\nauto result = co_await winrt::when_any(\r\n    somethingTask,\r\n    delayed_async_result&lt;decltype(somethingTask)&gt;(1s));\r\n<\/pre>\n<p>Or you can ask for an exception to be raised if the operation takes too long:<\/p>\n<pre>auto somethingTask = GetSomethingAsync();\r\nauto result = co_await winrt::when_any(\r\n    somethingTask,\r\n    delayed_timeout_exception&lt;decltype(somethingTask)&gt;(1s));\r\n<\/pre>\n<p>Having to pass a delayed result or delayed exception with a matching type as the thing you&#8217;re waiting for calls for a helper function to save you the typing:<\/p>\n<pre>template&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync when_complete_or_timeout(Async async, TimeSpan delay)\r\n{\r\n    return co_await winrt::when_any(async,\r\n        delayed_async_result&lt;Async&gt;(delay));\r\n}\r\n\r\ntemplate&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;!std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync when_complete_or_timeout(\r\n    Async async,\r\n    TimeSpan delay,\r\n    Result fallback = winrt_empty_value&lt;Result&gt;())\r\n{\r\n    return co_await winrt::when_any(async,\r\n        delayed_async_result&lt;Async&gt;(delay, fallback));\r\n}\r\n\r\ntemplate&lt;typename Async&gt;\r\nAsync when_complete_or_timeout_exception(\r\n    Async async,\r\n    TimeSpan delay)\r\n{\r\n    return co_await winrt::when_any(async,\r\n        delayed_timeout_exception&lt;Async&gt;(delay));\r\n}\r\n<\/pre>\n<p>In the discussion of the C# version of these helpers, I noted that if the operation times out, it nevertheless continues to run. Windows Runtime asynchronous activities support the <code>Cancel()<\/code> method, so you can tell them to abandon whatever they were doing. We can add that feature to our helper function, so that all the incomplete activities are cancelled.<\/p>\n<p>Note that we had been leaving our <code>delayed_...<\/code> tasks uncancelled, so the timers nevertheless continue to run and either complete with nobody listening, or raise an exception that nobody is listening to. If you&#8217;re doing a lot of timeouts, these extra threadpool timers will eventually drain, but you may not want them to accumulate in the first place.<\/p>\n<p>So let&#8217;s cancel everything before we finish.<\/p>\n<pre>template&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync delayed_async_result(\r\n    TimeSpan delay)\r\n{\r\n    (co_await winrt::get_cancellation_token()).enable_propagation();\r\n    co_await winrt::resume_after(delay);\r\n}\r\n\r\ntemplate&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;!std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync delayed_async_result(\r\n    TimeSpan delay,\r\n    Result fallback = winrt_empty_value&lt;Result&gt;())\r\n{\r\n    (co_await winrt::get_cancellation_token()).enable_propagation();\r\n    co_await winrt::resume_after(delay);\r\n    co_return fallback;\r\n}\r\n\r\ntemplate&lt;typename Async&gt;\r\nAsync delayed_timeout_exception(TimeSpan delay)\r\n{\r\n    (co_await winrt::get_cancellation_token()).enable_propagation();\r\n    co_await winrt::resume_after(delay);\r\n    throw winrt::hresult_error(HRESULT_FROM_WIN32(ERROR_TIMEOUT));\r\n}\r\n<\/pre>\n<p>To prevent timers from lingering, we enable cancellation propagation in our delayed result\/exception coroutines. That way, when they are cancelled, they cancel the timer immediately rather than leaving the timer running, only for it to have nothing to do when it expires.<\/p>\n<pre>template&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync when_complete_or_timeout(Async async, TimeSpan delay)\r\n{\r\n    auto timeout = delayed_async_result&lt;Async&gt;(delay);\r\n\r\n    auto cancel_async = wil::scope_exit([&amp;] { async.Cancel(); });\r\n    auto cancel_timeout = wil::scope_exit([&amp;] { timeout.Cancel(); });\r\n\r\n    return co_await winrt::when_any(async, timeout);\r\n}\r\n\r\ntemplate&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;!std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync when_complete_or_timeout(\r\n    Async async,\r\n    TimeSpan delay,\r\n    Result fallback = winrt_empty_value&lt;Result&gt;())\r\n{\r\n    auto timeout = delayed_async_result&lt;Async&gt;(delay, fallback);\r\n\r\n    auto cancel_async = wil::scope_exit([&amp;] { async.Cancel(); });\r\n    auto cancel_timeout = wil::scope_exit([&amp;] { timeout.Cancel(); });\r\n\r\n    return co_await winrt::when_any(async, timeout);\r\n}\r\n\r\ntemplate&lt;typename Async&gt;\r\nAsync when_complete_or_timeout_exception(\r\n    Async async,\r\n    TimeSpan delay)\r\n{\r\n    auto timeout = delayed_timeout_exception&lt;Async&gt;(delay);\r\n\r\n    auto cancel_async = wil::scope_exit([&amp;] { async.Cancel(); });\r\n    auto cancel_timeout = wil::scope_exit([&amp;] { timeout.Cancel(); });\r\n\r\n    return co_await winrt::when_any(async, timeout);\r\n}\r\n<\/pre>\n<p>After accepting the <code>Async<\/code> and creating our matching timeout, we use an RAII type to ensure that both are cancelled when the coroutine completes, even if it completes with an exception. Cancelling an already-completed Windows Runtime asynchronous activity has no effect, so we don&#8217;t have to keep track of which activity completed and which is being abandoned. We just cancel them all and let somebody else figure it out.<\/p>\n<p>There&#8217;s a lot of repetition in the version up above, so let&#8217;s try to shorten it up a bit.<\/p>\n<pre>template&lt;typename First, typename...Rest&gt;\r\nFirst when_any_cancel_others(First first, Rest...rest)\r\n{\r\n    auto cleanup = std::make_tuple(\r\n        wil::scope_exit([&amp;] { first.Cancel(); }),\r\n        wil::scope_exit([&amp;] { rest.Cancel(); })...);\r\n\r\n    return co_await winrt::when_any(first, rest...);\r\n}\r\n\r\ntemplate&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync when_complete_or_timeout(Async async, TimeSpan delay)\r\n{\r\n    return when_any_cancel_others(async,\r\n        [](TimeSpan delay) -&gt; Async {\r\n            (co_await winrt::get_cancellation_token()).enable_propagation();\r\n            co_await winrt::resume_after(delay);\r\n        }(delay));\r\n}\r\n\r\ntemplate&lt;\r\n    typename Async,\r\n    typename Result = decltype(std::declval&lt;Async&gt;().GetResults()),\r\n    typename std::enable_if_t&lt;!std::is_same_v&lt;Result, void&gt;, int&gt; = 0&gt;\r\nAsync when_complete_or_timeout(\r\n    Async async,\r\n    TimeSpan delay,\r\n    Result fallback = winrt_empty_value&lt;Result&gt;())\r\n{\r\n    return when_any_cancel_others(async,\r\n        [](TimeSpan delay, Result fallback) -&gt; Async {\r\n            (co_await winrt::get_cancellation_token()).enable_propagation();\r\n            co_await winrt::resume_after(delay);\r\n            co_return fallback;\r\n        }(delay, std::move(fallback)));\r\n}\r\n\r\ntemplate&lt;typename Async&gt;\r\nAsync when_complete_or_timeout_exception(\r\n    Async async,\r\n    TimeSpan delay)\r\n{\r\n    return when_any_cancel_others(async,\r\n        [](TimeSpan delay) -&gt; Async {\r\n            (co_await winrt::get_cancellation_token()).enable_propagation();\r\n            co_await winrt::resume_after(delay);\r\n            throw winrt::hresult_error(HRESULT_FROM_WIN32(ERROR_TIMEOUT));\r\n        }(delay));\r\n}\r\n<\/pre>\n<p><b>Exercise<\/b>: Why can&#8217;t we simplify <code>when_<wbr \/>any_<wbr \/>cancel_<wbr \/>others<\/code> to this?<\/p>\n<pre>template&lt;typename...Args&gt;\r\nauto when_any_cancel_others(Args...args)\r\n{\r\n    auto cleanup = std::make_tuple(\r\n        wil::scope_exit([&amp;] { args.Cancel(); })...);\r\n\r\n    return winrt::when_any(args...);\r\n}\r\n<\/pre>\n<p><b>Exercise 2<\/b>: Why not use perfect forwarding to avoid the extra <code>AddRef<\/code> and <code>Release<\/code>?<\/p>\n<pre>template&lt;typename First, typename...Rest&gt;\r\nstd::decay_t&lt;First&gt;\r\nwhen_any_cancel_others(First&amp;&amp; first, Rest&amp;...rest)\r\n{\r\n    auto cleanup = std::make_tuple(\r\n        wil::scope_exit([&amp;] { first.Cancel(); }),\r\n        wil::scope_exit([&amp;] { rest.Cancel(); })...);\r\n\r\n    return co_await winrt::when_any(\r\n        std::forward&lt;First&gt;(first),\r\n        std::forward&lt;Rest&gt;(rest)...);\r\n}\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Doing the same thing, just in another language.<\/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-106602","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Doing the same thing, just in another language.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/106602","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=106602"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/106602\/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=106602"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=106602"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=106602"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}