September 9th, 2021

Ordering asynchronous updates with coroutines, part 4: Bowing out, explicit version

Last time, we looked at the “Everybody tries, but only one wins” pattern, in which everyone calculates a result, but only the last one gets to save it. While this does work, we noted that there’s an inefficiency: Every calculation runs to completion, even if it has been superseded.

We can address that problem by re-checking after every coroutine resumption whether we have already lost. If so, we just give up.

bool Widget::KeepGoingAfterAwait(uint32_t counter)
{
    std::lock_guard guard{ m_mutex };
    return counter = m_counter;
}

winrt::IAsyncAction Widget::RecalcAsync()
{
    auto lifetime = get_strong();

    uint32_t counter;
    winrt::hstring messageId;
    winrt::hstring lang;
    {
        std::lock_guard guard{ m_mutex };
        counter = ++m_counter;
        messageId = m_messageId;
        lang = m_lang;
    }

    auto resolved = co_await ResolveLanguageAsync(lang);
    if (!KeepGoingAfterAwait(counter)) co_return;
    auto library = co_await GetResourceLibraryAsync(resolved);
    if (!KeepGoingAfterAwait(counter)) co_return;
    auto message = library.LookupResourceAsync(messageId);
    if (!KeepGoingAfterAwait(counter)) co_return;

    std::lock_guard guard{ m_mutex };
    if (m_counter == counter) {
        m_message = message;
    }
}

After every co_await, we check whether our counter is still current. If not, then it means that while we were co_awaiting, somebody else started a Refresh­Async which caused our refresh to become obsolete. Instead of proceeding with the work, only to reject it at the end, we just stop immediately.

The last Keep­Going­After­Await() check is redundant because we’re going to check one last time inside the lock, but I wrote it out anyway.

As we observed earlier, we can get rid of the locks if all accesses to the members are on a single thread.

bool Widget::KeepGoingAfterAwait(uint32_t counter)
{
    return counter = m_counter;
}

winrt::IAsyncAction Widget::RecalcAsync()
{
    auto lifetime = get_strong();

    uint32_t counter;
    winrt::hstring messageId;
    winrt::hstring lang;
    {
        std::lock_guard guard{ m_mutex };
        counter = ++m_counter;
        messageId = m_messageId;
        lang = m_lang;
    }

    auto resolved = co_await ResolveLanguageAsync(lang);
    if (!KeepGoingAfterAwait(counter)) co_return;
    auto library = co_await GetResourceLibraryAsync(resolved);
    if (!KeepGoingAfterAwait(counter)) co_return;
    auto message = library.LookupResourceAsync(messageId);
    if (!KeepGoingAfterAwait(counter)) co_return;

    m_message = message;
}

I removed the final check of the m_counter, since it is redundant with the Keep­Going­After­Await() that immediately precedes it.

Next time, we’ll see how this pattern is already covered by existing functionality.

Topics
Code

Author

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.

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Jacob Manaker

    Typo: “KeepGoingAfterAwait” should use “==”, not “=”, I think.

    • Ismo Salonen

      Yep, I spotted that too immediately. Very common mistake in the old days ( Programming since 1982)