April 15th, 2022

How can I co_await on a Windows Runtime async action or operation with a timeout?

Say you want to co_await on an IAsyncOperation but also want to abandon the await if it takes too long.

auto widgetOperation = GetWidgetAsync();
if (winrt::wait_for(widgetOperation, 15s) == AsyncStatus::Started)
{
    // timed out
    widgetOperation.Cancel(); // abandon the operation
} else {
    // will throw if operation failed or was cancelled
    auto widget = widgetOperation.GetResults();
}

The downside of this approach is that it blocks the thread for 15 seconds. This is bad enough for a background thread, since you are holding a thread hostage for the duration, but it’s really bad for a UI thread, since it makes your UI hang.

Instead, you can use when_any which creates a new IAsyncXxx that completes when any of its parameters completes. The first parameter is the widgetOperation, and the second is a coroutine that returns the same type of operation, but completes after a set timeout.

auto widgetOperation = GetWidgetAsync();
auto widgetTimeout = [] -> IAsyncOperation<Widget>
    {
        co_await winrt::resume_after(15s);
        co_return nullptr;
    }();
auto widget = co_await winrt::when_any(widgetOperation, widgetTimeout);

widgetOperation.Cancel();
widgetTimeout.Cancel();

if (!widget) {
    // timed out or GetWidgetAsync() returned nullptr
} else {
    // GetWidgetAsync() produced a widget
}

We create two IAsyncOperation<Widget>s. One is the operation we care about (GetWidgetAsync()) and the other is an operation that waits 15 seconds, and then completes with nullptr.

We then use winrt::when_any to await until anything completes. Once anything completes, we cancel everything. This has no effect on asynchronous operations that are completed, but it tells the still-incomplete operations to abandon their work if they can. Cancelling everything isn’t technically necessary, but it does avoid doing extra work that is going to be ignored anyway.

After all the tidying is done, we see if we actually got something. If the Get­Widget­Async() is taking too long, then the winning operation will be the widgetTimeout, and it completes with nullptr. If the Get­Widget­Async() finishes in under 15 seconds, then it will be the winning operation, and the result will be the thing it produced. That thing it produced could have been nullptr, so you can’t distinguish it from a timeout, but the idea here is that you picked the co_return at the end of the widgetTimeout lambda so that nullptr corresponds to what you would have wanted to do in the case of a timeout.

If you really need to distinguish between completing with nullptr and timing out, you could add an explicit flag.

auto timedOut = std::make_shared<bool>();
auto widgetOperation = GetWidgetAsync();
auto widgetTimeout = [](auto timedOut) -> IAsyncOperation<Widget>
    {
        co_await winrt::resume_after(15s);
        *timedOut = true;
        co_return nullptr;
    }(timedOut);
auto widget = co_await winrt::when_any(widgetOperation, widgetTimeout);

widgetOperation.Cancel();
widgetTimeout.Cancel();

if (*timedOut) {
    // timed out
} else {
    // GetWidgetAsync() produced something (possibly nullptr)
}

Or, perhaps more sneakily, you can check the status of the widgetTimeout operation.

auto widgetOperation = GetWidgetAsync();
auto widgetTimeout = [] -> IAsyncOperation<Widget>
    {
        co_await winrt::resume_after(15s);
        co_return nullptr;
    }();
auto widget = co_await winrt::when_any(widgetOperation, widgetTimeout);
auto timedOut = widgetTimeout.Status() == AsyncStatus::Completed;

widgetOperation.Cancel();
widgetTimeout.Cancel();

if (timedOut) {
    // timed out
} else {
    // GetWidgetAsync() produced something (possibly nullptr)
}

We’ll expand upon this pattern in a few weeks, so stay tuned. (Or “tune out”, if that’s more to your liking.)

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.

3 comments

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

  • Marcus Rahilly

    Apologies as this is unrelated to this post but is related to https://devblogs.microsoft.com/oldnewthing/20210809-00/?p=105539

    MessageDialog msgdlg{ L”Choose a color”, L”Async example” };
    msgdlg.Commands().Append(UICommand{ L”Red”, nullptr, box_value(Colors::Red()) });

    IUICommand showTask = co_await msgdlg.ShowAsync(); // C2039 promise_type

    I am getting a C2039 promise_type coroutine error when winrt/Windows.Foundation.h is included. Should I include some other implementation header?

  • Martin Bonner

    In the final approach, isn’t there a race? widgetOperation completes after just before the timeout, the wait_any returns – but by the time you check if the timeout has completed it has. (I suppose this isn’t much of a problem, if means in a very rare condition you will think the operation timed out when it had succeeded )

  • 紅樓鍮 · Edited

    I wonder whether it would be more useful to have a when_any function that returns a low-overhead awaitable instead of an IAsyncOperation (you can always recover an IAsyncOperation by wrapping it inside a coroutine lambda or member function), and that await_resume()s a std::variant to indicate which input awaitable succeeded (which IAsyncOperation can’t do).