Preliminary. ThinkMeta ConcurrentTasks and this documentation are preliminary; everything is subject to change.

WinRT operations

On this page

Many Windows APIs exist only as Windows Runtime (WinRT) classes, and their slow operations are asynchronous: FindAllAsync, FromIdAsync, GetGattServicesAsync and so on return an IAsyncOperation instead of a result. WinRT::Await waits for such an operation from a task: the task is suspended until the operation completes, and the scheduler thread runs other tasks meanwhile. The examples come from bluetooth-devices.cpp, the Bluetooth devices sample.

The program uses these modules and names:

#include <ThinkMeta.ConcurrentTasks/Core.h>
import ThinkMeta.ConcurrentTasks.Synchronization.TaskGroup;
import ThinkMeta.ConcurrentTasks.Tasks.Task;
import ThinkMeta.ConcurrentTasks.WinRT.Await;

namespace TC = ThinkMeta::ConcurrentTasks;
using TC::WinRT::Await;
using namespace winrt::Windows::Devices::Bluetooth;
using namespace winrt::Windows::Devices::Enumeration;

The library module needs no WinRT headers. Your program includes the C++/WinRT headers it uses (they come with the Windows SDK) and links windowsapp.lib.

Waiting for an operation

Pass the started operation to Await; it returns what GetResults returns, nothing for an action:

class QueryTask : public TC::Tasks::Task
{
    // ...
    void Run() noexcept override
    {
        const auto start = GetTickCount64();
        try {
            const auto properties = winrt::single_threaded_vector<winrt::hstring>({AddressProperty});
            devices_              = Await(DeviceInformation::FindAllAsync(selector_, properties, DeviceInformationKind::AssociationEndpoint));
            CountAllServices();
        }
        catch (const winrt::hresult_error& e) {
            error_ = e.code();
        }
        milliseconds_ = GetTickCount64() - start;
    }
    // ...
};

Await accepts every type that satisfies AsyncOperationType: IAsyncAction, IAsyncOperation<TResult> and their …WithProgress variants.

Situation What Await does
The operation has already finished Returns the result at once; the task is not suspended
The operation is still running Sets its Completed handler and suspends the task until the handler runs
The operation failed GetResults throws winrt::hresult_error with the operation's error code
The operation was canceled GetResults throws winrt::hresult_canceled

Important

Await suspends the calling task, so it may only be called from a task or from an OS thread after ConvertThreadToTask. It sets the operation's Completed handler, which an operation accepts only once: await each operation once, and do not also co_await it or register a handler of your own.

No colored functions

With C++/WinRT coroutines, a function that waits must itself be a coroutine: it returns an IAsyncOperation, uses co_await, and so does every function that calls it, up to the top. With Await the waiting function stays an ordinary function with an ordinary return type:

// Ordinary functions that wait: each Await suspends the calling task until the operation has
// completed. Cached mode reads what Windows already knows and does not connect to the device.
// Returns -1 if the services could not be read.
static std::int32_t CountRfcommServices(const winrt::hstring& id)
{
    const auto device = Await(BluetoothDevice::FromIdAsync(id));
    if (!device)
        return -1;

    const auto result = Await(device.GetRfcommServicesAsync(BluetoothCacheMode::Cached));
    return result.Error() == BluetoothError::Success ? static_cast<std::int32_t>(result.Services().Size()) : -1;
}

static std::int32_t CountGattServices(const winrt::hstring& id)
{
    const auto device = Await(BluetoothLEDevice::FromIdAsync(id));
    if (!device)
        return -1;

    const auto result = Await(device.GetGattServicesAsync(BluetoothCacheMode::Cached));
    return result.Status() == GenericAttributeProfile::GattCommunicationStatus::Success ? static_cast<std::int32_t>(result.Services().Size()) : -1;
}

// A plain function pointer, although the functions behind it wait.
using CountServices = std::int32_t (*)(const winrt::hstring& id);

Both functions fit a plain function pointer, are called from an ordinary loop and report errors with ordinary exceptions. Their callers do not know that they wait.

C++/WinRT coroutines WinRT::Await in a task
A function that waits returns IAsyncOperation<T>, uses co_await returns T, calls Await
Its callers must become coroutines too stay unchanged
Function pointers, callbacks, virtual functions must be declared with IAsyncOperation<T> keep their ordinary signatures
Locals and reference parameters in a heap frame per call; a reference may dangle after a suspension on the task's stack, valid across the wait
Several operations at once start them, then co_await each or winrt::when_all (which discards the results) one task per operation and a TaskGroup
Where the code goes on after the wait on the thread that completed the operation (a WinRT thread-pool thread), unless it captured an apartment_context on a scheduler thread
Waiting synchronously .get() blocks the thread, and must not be called on a single-threaded apartment does not exist: Await never blocks the thread

Many operations at once

Each waiting task costs a fiber, not a thread. To run operations at the same time, give each one a task and wait for the group:

class QueryTask : public TC::Tasks::Task
{
    // ...
    // One task per device; all of them wait at the same time.
    void CountAllServices()
    {
        const auto numDevices = devices_.Size();
        deviceTasks_          = std::make_unique<DeviceTask[]>(numDevices);

        // Read every ID before the first task starts: an exception must not leave the group while
        // started tasks still report to it.
        for (std::uint32_t i = 0; i < numDevices; ++i)
            deviceTasks_[i].Initialize(devices_.GetAt(i).Id(), countServices_);

        TC::Synchronization::TaskGroup_s deviceGroup;
        for (std::uint32_t i = 0; i < numDevices; ++i) {
            deviceGroup.AddTask(&deviceTasks_[i]);
            TC::GetTaskScheduler()->AddTask(&deviceTasks_[i], TC::TaskPriority::Normal);
        }

        deviceGroup.Wait();
    }
    // ...
};

The sample runs four device searches this way, and each search starts one task per device it finds; two scheduler threads carry all of them.

Apartments

WinRT objects live in COM apartments. A task may resume on another scheduler thread than the one it was suspended on, so use agile objects only. Most WinRT classes are agile; objects bound to a single-threaded apartment (STA), such as UI objects, are not.

The scheduler threads must belong to the multithreaded apartment (MTA). The simplest way is to initialize it once for the process: while the MTA exists, threads that never called CoInitializeEx belong to it implicitly.

int main()
{
    // The scheduler threads join the multithreaded apartment implicitly.
    winrt::init_apartment(winrt::apartment_type::multi_threaded);

    QueryTask queries[] = {
        {L"Bluetooth, paired", BluetoothDevice::GetDeviceSelectorFromPairingState(true), CountRfcommServices},
        {L"Bluetooth, not paired", BluetoothDevice::GetDeviceSelectorFromPairingState(false), CountRfcommServices},
        {L"Bluetooth LE, paired", BluetoothLEDevice::GetDeviceSelectorFromPairingState(true), CountGattServices},
        {L"Bluetooth LE, not paired", BluetoothLEDevice::GetDeviceSelectorFromPairingState(false), CountGattServices},
    };

    TC::ConvertThreadToTask();

    TC::SchedulerInfo info      = {.numberOfThreads = 2};
    auto              scheduler = TC::CreateTaskScheduler(TC::SchedulerKind::Default, &info);
    scheduler->Start();

    const auto start = GetTickCount64();

    TC::Synchronization::TaskGroup_s queryGroup;
    for (auto& query : queries) {
        queryGroup.AddTask(&query);
        scheduler->AddTask(&query, TC::TaskPriority::Normal);
    }
    queryGroup.Wait();

    const auto milliseconds = GetTickCount64() - start;

    scheduler->Stop();
    scheduler->Destroy();
    TC::ConvertTaskToThread();
    // ...
}

Alternatives are CoIncrementMTAUsage, which keeps the MTA alive without initializing a thread, or CoInitializeEx(nullptr, COINIT_MULTITHREADED) on each scheduler thread from a thread callback.

No timeouts

There is no Await with a time limit. The Completed handler refers to state on the waiting task's stack, so the task cannot leave before the operation has completed. To limit the time, call Cancel on the operation from another task or a timer: the operation then completes as canceled, and Await throws winrt::hresult_canceled.

See also