Sharing the result of a single Windows Runtime IAsyncOperation among multiple coroutines, part 3

The Old New Thing (Raymond Chen) News

Summary

The article discusses a C++/WinRT pattern for caching the result of a Windows Runtime IAsyncOperation, including handling failures, so that multiple coroutines can share the cached result or exception.

<p>Last time, <a title="Sharing the result of a single Windows Runtime IAsyncOperation among multiple coroutines, part 2" href="https://devblogs.microsoft.com/oldnewthing/20260528-00/?p=112365"> we wrote a coroutine function that cached the result of another coroutine</a>, but it only cached successful calls. It didn&#8217;t cache failures, so if the inner coroutine fails, the outer one will simply try again the next time it is called. But what if we want to call the inner coroutine only once and cache the failure, too?</p> <p>We now have three states: Never tried, tried with success (and cache the success result), and tried with failure (and cache the failure). We can represent that as a variant with three types, using <code>std::<wbr />monostate</code> to represent the &#8220;never tried&#8221; state.¹</p> <pre>struct Widget : WidgetT&lt;Widget&gt; { std::variant&lt;std::monostate, winrt::Thing, std::exception_ptr&gt; m_thing; wil::unique_event m_busy{ wil::EventOptions::Signaled }; // auto-reset, initially signaled IAsyncOperation&lt;winrt::Thing&gt; GetThingAsync() { auto lifetime = get_strong(); co_await winrt::resume_on_signal(m_busy.get()); auto not_busy = m_busy.SetEvent_scope_exit(); // If haven't tried, then this is our chance. if (m_thing.holds_alternative&lt;std::monostate&gt;()) { try { m_thing = co_await GetThingWorkerAsync(); } catch (...) { m_thing = std::current_exception(); } } // Return the cached result or cached failure. if (auto thing = std::get_if&lt;winrt::Thing&gt;(&amp;m_thing)) { co_return *thing; } else { std::rethrow_exception(std::get&lt;std::exception_ptr&gt;()); } } }; </pre> <p>After getting past our serialization, we check whether the <code>m_thing</code> holds a <code>std::<wbr />monostate</code>, meaning that we haven&#8217;t tried getting the thing yet. If so, then this is the first time through the function, so we will call <code>Get­Thing­Worker­Async</code> and save the answer in the <code>m_thing</code>. If the call fails, then we save the exception in the <code>m_thing</code>.</p> <p>Regardeless of whether this is the first or subsequent call, we know that by the time we get past the first <code>if</code>, the <code>m_thing</code> is definitely not a <code>std::<wbr />monostate</code>. If it has a <code>winrt::<wbr />Thing</code>, then we return that cached thing. Otherwise, it must be a <code>std::<wbr />exception_<wbr />ptr</code>, so we rethrow that exception.</p> <p>If we know that <code>Get­Thing­Worker­Async</code> never succeeds with <code>nullptr</code>, we can simplify the code by having separate variables (one for the non-null successful result and one for the exception pointer on failure), knowing that at most one of them will be non-null. And if both are null, then it means we haven&#8217;t attempted the call yet.</p> <pre>struct Widget : WidgetT&lt;Widget&gt; { <span style="border: solid 1px currentcolor; border-bottom: none;">winrt::Thing m_thing{ nullptr };</span> <span style="border: solid 1px currentcolor; border-top: none;">std::exception_ptr m_ex; </span> wil::unique_event m_busy{ wil::EventOptions::Signaled }; // auto-reset, initially signaled IAsyncOperation&lt;winrt::Thing&gt; GetThingAsync() { auto lifetime = get_strong(); co_await winrt::resume_on_signal(m_busy.get()); auto not_busy = m_busy.SetEvent_scope_exit(); // If haven't tried, then this is our chance. if (<span style="border: solid 1px currentcolor;">!m_thing &amp;&amp; !m_ex</span>) { try { m_thing = co_await GetThingWorkerAsync(); <span style="border: solid 1px currentcolor;">assert(m_thing);</span> } catch (...) { <span style="border: solid 1px currentcolor;">m_ex</span> = std::current_exception(); } } <span style="border: solid 1px currentcolor; border-bottom: none;">// Return the cached result or cached failure.</span> <span style="border: 1px currentcolor; border-style: none solid;">if (m_thing) { </span> <span style="border: 1px currentcolor; border-style: none solid;"> co_return m_thing; </span> <span style="border: 1px currentcolor; border-style: none solid;">} else { </span> <span style="border: 1px currentcolor; border-style: none solid;"> std::rethrow_exception(m_ex); </span> <span style="border: solid 1px currentcolor; border-top: none;">} </span> } }; </pre> <p>¹ Bonus reading about <code>std::<wbr />monostate</code>: <a title="What's the point of std::monostate? You can't do anything with it!" href="https://devblogs.microsoft.com/oldnewthing/20240708-00/?p=109959"> What&#8217;s the point of <code>std::<wbr />monostate</code>? You can&#8217;t do anything with it</a>.</p> <p>The post <a href="https://devblogs.microsoft.com/oldnewthing/20260529-00/?p=112368">Sharing the result of a single Windows Runtime IAsyncOperation among multiple coroutines, part 3</a> appeared first on <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>.</p>
Original Article
View Cached Full Text

Cached at: 05/30/26, 08:58 AM

# Sharing the result of a single Windows Runtime IAsyncOperation among multiple coroutines, part 3 - The Old New Thing Source: [https://devblogs.microsoft.com/oldnewthing/20260529-00?p=112368](https://devblogs.microsoft.com/oldnewthing/20260529-00?p=112368) Last time,[we wrote a coroutine function that cached the result of another coroutine](https://devblogs.microsoft.com/oldnewthing/20260528-00/?p=112365), but it only cached successful calls\. It didn’t cache failures, so if the inner coroutine fails, the outer one will simply try again the next time it is called\. But what if we want to call the inner coroutine only once and cache the failure, too? We now have three states: Never tried, tried with success \(and cache the success result\), and tried with failure \(and cache the failure\)\. We can represent that as a variant with three types, using`std::monostate`to represent the “never tried” state\.¹ ``` struct Widget : WidgetT<Widget> { std::variant<std::monostate, winrt::Thing, std::exception_ptr> m_thing; wil::unique_event m_busy{ wil::EventOptions::Signaled }; // auto-reset, initially signaled IAsyncOperation<winrt::Thing> GetThingAsync() { auto lifetime = get_strong(); co_await winrt::resume_on_signal(m_busy.get()); auto not_busy = m_busy.SetEvent_scope_exit(); // If haven't tried, then this is our chance. if (m_thing.holds_alternative<std::monostate>()) { try { m_thing = co_await GetThingWorkerAsync(); } catch (...) { m_thing = std::current_exception(); } } // Return the cached result or cached failure. if (auto thing = std::get_if<winrt::Thing>(&m_thing)) { co_return *thing; } else { std::rethrow_exception(std::get<std::exception_ptr>()); } } }; ``` After getting past our serialization, we check whether the`m\_thing`holds a`std::monostate`, meaning that we haven’t tried getting the thing yet\. If so, then this is the first time through the function, so we will call`Get­Thing­Worker­Async`and save the answer in the`m\_thing`\. If the call fails, then we save the exception in the`m\_thing`\. Regardeless of whether this is the first or subsequent call, we know that by the time we get past the first`if`, the`m\_thing`is definitely not a`std::monostate`\. If it has a`winrt::Thing`, then we return that cached thing\. Otherwise, it must be a`std::exception\_ptr`, so we rethrow that exception\. If we know that`Get­Thing­Worker­Async`never succeeds with`nullptr`, we can simplify the code by having separate variables \(one for the non\-null successful result and one for the exception pointer on failure\), knowing that at most one of them will be non\-null\. And if both are null, then it means we haven’t attempted the call yet\. ``` struct Widget : WidgetT<Widget> { winrt::Thing m_thing{ nullptr }; std::exception_ptr m_ex; wil::unique_event m_busy{ wil::EventOptions::Signaled }; // auto-reset, initially signaled IAsyncOperation<winrt::Thing> GetThingAsync() { auto lifetime = get_strong(); co_await winrt::resume_on_signal(m_busy.get()); auto not_busy = m_busy.SetEvent_scope_exit(); // If haven't tried, then this is our chance. if (!m_thing && !m_ex) { try { m_thing = co_await GetThingWorkerAsync(); assert(m_thing); } catch (...) { m_ex = std::current_exception(); } } // Return the cached result or cached failure. if (m_thing) { co_return m_thing; } else { std::rethrow_exception(m_ex); } } }; ``` ¹ Bonus reading about`std::monostate`:[What’s the point of`std::monostate`? You can’t do anything with it](https://devblogs.microsoft.com/oldnewthing/20240708-00/?p=109959)\. ### Category ### Topics ## Author ![Raymond Chen](https://devblogs.microsoft.com/oldnewthing/wp-content/uploads/sites/38/2019/02/RaymondChen_5in-150x150.jpg) Raymond has been involved in the evolution of Windows for more than 30 years\. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie\-jeebies\. The Web site spawned a book, coincidentally also titled The Old New Thing \(Addison Wesley 2007\)\. He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information\.

Similar Articles

Cancellation of Windows Runtime activities is asynchronous

The Old New Thing (Raymond Chen)

This article explains why cancellation of Windows Runtime asynchronous activities is asynchronous, using code examples to illustrate how it avoids deadlocks, especially when progress callbacks trigger cancellation.