0

I'm looking for a way to compose asynchronous operations. The ultimate goal is to execute an asynchronous operation, and either have it run to completion, or return after a user-defined timeout.

For exemplary purposes, assume that I'm looking for a way to combine the following coroutines1:

IAsyncOperation<IBuffer> read(IBuffer buffer, uint32_t count)
{
    auto&& result{ co_await socket_.InputStream().ReadAsync(buffer, count, InputStreamOptions::None) };
    co_return result;
}

with socket_ being a StreamSocket instance.

And the timeout coroutine:

IAsyncAction timeout()
{
    co_await 5s;
}

I'm looking for a way to combine these coroutines in a way, that returns as soon as possible, either once the data has been read, or the timeout has expired.

These are the options I have evaluated so far:

  • C++20 coroutines: As far as I understand P1056R0, there is currently no library or language feature "to enable creation and composition of coroutines".
  • Windows Runtime supplied asynchronous task types, ultimately derived from IAsyncInfo: Again, I didn't find any facilities that would allow me to combine the tasks the way I need.
  • Concurrency Runtime: This looks promising, particularly the when_any function template looks to be exactly what I need.

From that it looks like I need to go with the Concurrency Runtime. However, I'm having a hard time bringing all the pieces together. I'm particularly confused about how to handle exceptions, and whether cancellation of the respective other concurrent task is required.

The question is two-fold:

  • Is the Concurrency Runtime the only option (UWP application)?
  • What would an implementation look like?

1 The methods are internal to the application. It is not required to have them return Windows Runtime compatible types.

IInspectable
  • 46,945
  • 8
  • 85
  • 181

2 Answers2

1

I think the easiest would be to use the concurrency library. You need to modify your timeout to return the same type as the first method, even if it returns null.

(I realize this is only a partial answer...)

My C++ sucks, but I think this is close...

array<task<IBuffer>, 2> tasks =
{
concurrency::create_task([]{return read(buffer, count).get();}),
concurrency::create_task([]{return modifiedTimeout.get();})
};

concurrency::when_any(begin(tasks), end(tasks)).then([](IBuffer buffer)
{ 
    //do something 
});

Lee McPherson
  • 931
  • 6
  • 20
  • That looks promising, although it doesn't work out the details with respect to cancellation and exception propagation. Choosing a `std::optional` as the return type of either `task` looks like a valid option. Note, that `when_any` returns a `std::pair`, so that needs to be used in the `then` continuation. I'll see if I can work out the remaining details with the information you've provided. – IInspectable Apr 05 '19 at 08:10
  • Since you're doing event based stuff, an alternative you might want to look at is ReactiveX. There's a C++ library for it. But, you basically create an Observable for your IBuffer and an Observable for your timer (that also emits IBuffer, but null). Then you Merge them and Take(1). This will "complete" the merged Observable after which you can handle cancellation of the one not yet finished. Exceptions are easy to catch as well, although I have never tried with C++, only C#. If you've never used Observables from ReactiveX, beware there is a steep learning curve – Lee McPherson Apr 05 '19 at 20:40
  • Finally got around to doing some testing with the information you provided. Still a few open ends, I haven't been able to fully understand. I was originally planning to update your answer, but it turned out to become much longer than I thought, and I didn't want to turn your answer into something you may not agree with. ReactiveX looks useful, even though it is a bit too much for what I (currently) need. And I'm not too thrilled about adding yet another heavily template-based library. It's near impossible to regularly run code analysis on a C++/WinRT project as is already... – IInspectable Apr 16 '19 at 18:06
1

As suggested by Lee McPherson in another answer, the Concurrency Runtime looks like a viable option. It provides tasks, that can be combined with others, chained up using continuations, as well as seamlessly integrate with the Windows Runtime asynchronous model (see Creating Asynchronous Operations in C++ for UWP Apps). As a bonus, including the <pplawait.h> header provides adapters for concurrency::task class template instantiations to be used as C++20 coroutine awaitables.

I wasn't able to answer all of the questions, but this is what I eventually came up with. For simplicity (and ease of verification) I'm using Sleep in place of the actual read operation, and return an int instead of an IBuffer.

Composition of tasks

The ConcRT provides several ways to combine tasks. Given the requirements concurrency::when_any can be used to create a task that returns, when any of the supplied tasks completes. When only 2 tasks are supplied as input, there's also a convenience operator (operator||) available.

Exception propagation

Exceptions raised from either of the input tasks do not count as a successful completion. When used with the when_any task, throwing an exception will not suffice the wait condition. As a consequence, exceptions cannot be used to break out of combined tasks. To deal with this I opted to return a std::optional, and raise appropriate exceptions in a then continuation.

Task cancellation

This is still a mystery to me. It appears that once a task satisfies the wait condition of the when_any task, there is no requirement to cancel the respective other outstanding tasks. Once those complete (successfully or otherwise), they are silently dealt with.

Following is the code, using the simplifications mentioned earlier. It creates a task consisting of the actual workload and a timeout task, both returning a std::optional. The then continuation examines the return value, and throws an exception in case there isn't one (i.e. the timeout_task finished first).

#include <Windows.h>

#include <cstdint>
#include <iostream>
#include <optional>
#include <ppltasks.h>
#include <stdexcept>

using namespace concurrency;

task<int> read_with_timeout(uint32_t read_duration, uint32_t timeout)
{
    auto&& read_task
    {
        create_task([read_duration]
            {
                ::Sleep(read_duration);
                return std::optional<int>{42};
            })
    };
    auto&& timeout_task
    {
        create_task([timeout]
            {
                ::Sleep(timeout);
                return std::optional<int>{};
            })
    };

    auto&& task
    {
        (read_task || timeout_task)
        .then([](std::optional<int> result)
            {
                if (!result.has_value())
                {
                    throw std::runtime_error("timeout");
                }
                return result.value();
            })
    };
    return task;
}

The following test code

int main()
{
    try
    {
        auto res1{ read_with_timeout(3000, 5000).get() };
        std::cout << "Succeeded. Result = " << res1 << std::endl;
        auto res2{ read_with_timeout(5000, 3000).get() };
        std::cout << "Succeeded. Result = " << res2 << std::endl;
    }
    catch( std::runtime_error const& e )
    {
        std::cout << "Failed. Exception = " << e.what() << std::endl;
    }
}

produces this output:

Succeeded. Result = 42
Failed. Exception = timeout
IInspectable
  • 46,945
  • 8
  • 85
  • 181