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.