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

Bluetooth devices

On this page

Four device searches run at the same time – Bluetooth and Bluetooth LE, paired and not paired – and each search starts one task per device it finds to count the device's services. Every wait is a WinRT operation, and every one is waited for with WinRT::Await from an ordinary function: no co_await, no coroutine return types, no blocked threads. Two scheduler threads carry all searches and lookups.

Functions that wait

// 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);

Each Await suspends the calling task until the operation has completed and returns its result. The functions return a plain std::int32_t, so a plain function pointer can hold either of them. BluetoothCacheMode::Cached reads what Windows already knows about the device and does not connect to it.

One task per device

// Counts the services of one device.
class DeviceTask : public TC::Tasks::Task
{
public:
    void Initialize(winrt::hstring id, CountServices countServices) noexcept
    {
        id_            = std::move(id);
        countServices_ = countServices;
    }

    void Run() noexcept override
    {
        try {
            serviceCount_ = countServices_(id_);
        }
        catch (const winrt::hresult_error&) {
            // the device could not be opened; its services stay unknown
        }
    }

    std::int32_t ServiceCount() const noexcept { return serviceCount_; }

private:
    winrt::hstring id_;
    CountServices  countServices_ = nullptr;
    std::int32_t   serviceCount_  = -1;
};

A device that cannot be opened makes Await throw winrt::hresult_error; the task catches it and leaves the count unknown. An exception must not escape a task's Run.

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;
    }
    // ...
};

The search task waits for FindAllAsync, then starts the device tasks and waits for them:

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 device tasks are added from inside a task with GetTaskScheduler. The TaskGroup_s is a local of the waiting task and may be destroyed right after its Wait.

Running the searches

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();
    // ...
}

winrt::init_apartment puts the main thread in the multithreaded apartment; the scheduler threads then belong to it implicitly. See WinRT operations.

Full program

Show the whole program
// Bluetooth devices: four WinRT device searches run as tasks at the same time, then one task
// per device found counts its services. Every wait is WinRT::Await, called from ordinary
// functions. The devices found depend on the machine, so the program checks only that no
// search failed.
#include <Windows.h>

#include <cstdint>
#include <cstdio>
#include <memory>
#include <utility>

#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h>
#include <winrt/Windows.Devices.Bluetooth.Rfcomm.h>
#include <winrt/Windows.Devices.Bluetooth.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Foundation.h>

#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;

// 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);

// Counts the services of one device.
class DeviceTask : public TC::Tasks::Task
{
public:
    void Initialize(winrt::hstring id, CountServices countServices) noexcept
    {
        id_            = std::move(id);
        countServices_ = countServices;
    }

    void Run() noexcept override
    {
        try {
            serviceCount_ = countServices_(id_);
        }
        catch (const winrt::hresult_error&) {
            // the device could not be opened; its services stay unknown
        }
    }

    std::int32_t ServiceCount() const noexcept { return serviceCount_; }

private:
    winrt::hstring id_;
    CountServices  countServices_ = nullptr;
    std::int32_t   serviceCount_  = -1;
};

class QueryTask : public TC::Tasks::Task
{
public:
    QueryTask(const wchar_t* title, winrt::hstring selector, CountServices countServices) noexcept
        : title_(title),
          selector_(std::move(selector)),
          countServices_(countServices)
    {}

    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;
    }

    bool Failed() const noexcept { return error_ < 0; }

    void Print() const noexcept
    {
        std::wprintf(L"%ls (%llu ms)\n", title_, milliseconds_);
        if (Failed()) {
            std::wprintf(L"  error 0x%08X\n", static_cast<std::uint32_t>(error_));
            return;
        }
        if (devices_.Size() == 0)
            std::wprintf(L"  none\n");

        for (std::uint32_t i = 0; i < devices_.Size(); ++i) {
            const auto device  = devices_.GetAt(i);
            const auto address = winrt::unbox_value_or<winrt::hstring>(device.Properties().TryLookup(AddressProperty), L"?");
            const auto count   = deviceTasks_[i].ServiceCount();

            std::wprintf(L"  %ls  %-24ls ", address.c_str(), device.Name().empty() ? L"(no name)" : device.Name().c_str());
            if (count >= 0)
                std::wprintf(L"%2d services\n", count);
            else
                std::wprintf(L" ? services\n");
        }
    }

private:
    static constexpr const wchar_t* AddressProperty = L"System.Devices.Aep.DeviceAddress";

    // 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();
    }

    const wchar_t*                title_;
    winrt::hstring                selector_;
    CountServices                 countServices_;
    DeviceInformationCollection   devices_{nullptr};
    std::unique_ptr<DeviceTask[]> deviceTasks_;
    winrt::hresult                error_;
    std::uint64_t                 milliseconds_ = 0;
};

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();

    bool ok = true;
    for (const auto& query : queries) {
        query.Print();
        ok = ok && !query.Failed();
    }

    std::wprintf(L"All searches took %llu ms (%ls)\n", milliseconds, ok ? L"ok" : L"FAILED");
    return ok ? 0 : 1;
}

Source and project on GitHub

Build and run

A console program. Besides the core's props file and the library reference (see Set up a project) it links windowsapp.lib for C++/WinRT; the headers come with the Windows SDK. It needs a Bluetooth adapter, and the devices it prints are those near the machine:

Bluetooth, paired (172 ms)
  d0:1b:49:39:71:bf  Galaxy A32 5G             7 services
  bc:37:d3:05:24:52  Powerbeats Pro            2 services
Bluetooth, not paired (30031 ms)
  none
Bluetooth LE, paired (63 ms)
  d5:77:7a:e0:8d:30  DSV2 Pro                  6 services
Bluetooth LE, not paired (32656 ms)
  00:a0:50:a6:36:76  D1533256                  3 services
  60:74:f4:a1:0d:30  GBK_H6159_0D30            ? services
All searches took 32687 ms (ok)

The searches for devices that are not paired take about 30 seconds, the time Windows spends looking for them. Since all searches run at the same time, the whole program takes about as long as the slowest one.

What to notice

  • No function coloring. CountGattServices would be an IAsyncOperation<std::int32_t> coroutine in standard C++/WinRT, and so would CountAllServices and Run that call it. Here only the tasks know that something waits.
  • Waiting costs a fiber, not a thread. Up to four searches and all their device lookups wait at the same time on two scheduler threads.
  • Agile objects. A task may continue on the other scheduler thread after a wait. WinRT's device classes are agile, so that is fine; objects bound to a single-threaded apartment are not.
  • Lifetimes. The query tasks live in main until after Stop; the device tasks live in their query task until its group has been waited for.

See also