Расширенные возможности параллелизма и асинхронности с C++/WinRT

В этом разделе описываются расширенные сценарии с параллелизмом и асинхронностью в C++/WinRT.

Чтобы ознакомиться с этой темой, сначала ознакомьтесь с параллелизмом и асинхронными операциями.

Передача задач в пул потоков Windows

Корутина — это функция, как и любая другая: вызывающий код блокируется до тех пор, пока функция не вернет ему управление. И, первая возможность для корутины вернуться является первой co_await, co_returnили co_yield.

Итак, прежде чем выполнять в сопрограмме ресурсоёмкую вычислительную работу, необходимо вернуть управление вызывающему коду (иными словами, ввести точку приостановки), чтобы вызывающий код не блокировался. Если вы ещё не делаете этого с помощью co_await какой-либо другой операции, то можете co_await вызвать функцию winrt::resume_background. Это возвращает элемент управления вызывающей стороне, а затем немедленно возобновляет выполнение в потоке пула потоков.

Пул потоков, используемый в реализации, — это низкоуровневый пул потоков Windows, поэтому он оптимально эффективен.

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

Программирование с учетом сходства потоков

Этот сценарий развивает предыдущий. Вы передаёте часть работы в пул потоков, но при этом хотите отображать ход выполнения в пользовательском интерфейсе (UI).

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

Приведенный выше код вызывает исключение winrt::hresult_wrong_thread, так как TextBlock должен обновляться из потока, который его создал, а именно из потока UI. Одним из решений является захват контекста потока, в котором изначально называлась наша корутин. Для этого создайте экземпляр объекта winrt::apartment_context , выполните фоновую работу, а затем co_awaitapartment_context , чтобы вернуться к контексту вызова.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

Если приведённая выше корутина вызывается в потоке пользовательского интерфейса, который создал TextBlock, то этот приём работает. В приложении будет много случаев, когда вы уверены в этом.

В качестве более общего решения для обновления пользовательского интерфейса, которое охватывает случаи, когда вы не уверены, из какого потока выполняется вызов, можно co_await использовать функцию winrt::resume_foreground, чтобы переключиться на конкретный поток переднего плана. В приведенном ниже примере кода мы указываем поток переднего плана, передавая очередь диспетчера, связанную с TextBlock (обращаясь к его свойству DispatcherQueue). Реализация winrt::resume_foreground вызывает DispatcherQueue.TryEnqueue в этом объекте очереди диспетчера для выполнения работы, выполняемой после него в корутине.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.DispatcherQueue());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

Функция winrt::resume_foreground принимает необязательный параметр приоритета. Если вы используете этот параметр, шаблон, показанный выше, подходит. Если нет, можно просто упростить co_await winrt::resume_foreground(someDispatcherObject); до co_await someDispatcherObject;.

Контексты выполнения, возобновление и переключение в корутине

Широко говоря, после точки приостановки в корутине исходный поток выполнения может уйти и возобновление может произойти на любом потоке (другими словами, любой поток может вызвать метод Completed для асинхронной операции).

Но если вы co_await любой из четырех типов асинхронных операций среда выполнения Windows (IAsyncXxx), то C++/WinRT захватывает вызывающий контекст в точке, где вы co_await. И это гарантирует, что вы все еще находитесь в этом контексте при возобновлении продолжения. C++/WinRT делает это, проверяя, находитесь ли вы уже в вызывающем контексте, и, если нет, переключаясь на него. Если до co_await вы выполнялись в потоке однопоточного апартамента (STA), то и после этого будете выполняться в том же потоке; если до co_await вы выполнялись в потоке многопоточного апартамента (MTA), то и после этого будете выполняться в одном из таких потоков.

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

Причина, по которой можно полагаться на такое поведение, заключается в том, что C++/WinRT предоставляет код для адаптации этих типов асинхронных операций среда выполнения Windows к поддержке корутин в языке C++ (эти фрагменты кода называются адаптерами ожидания). Остальные ожидаемые типы в C++/WinRT — это просто вспомогательные оболочки пула потоков; поэтому они завершаются в пуле потоков.

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

Если вы co_await используете какой-либо другой тип — даже в реализации корутины C++/WinRT, — то адаптеры предоставляет другая библиотека, и вам потребуется понять, как именно работают эти адаптеры с точки зрения возобновления выполнения и контекстов.

Чтобы свести переключения контекста к минимуму, можно использовать некоторые из приёмов, которые мы уже рассмотрели в этой теме. Давайте посмотрим на некоторые иллюстрации этого. В следующем примере псевдокода показана общая схема обработчика событий, который вызывает API среда выполнения Windows для загрузки изображения, переключается на фоновый поток для обработки этого изображения, а затем возвращается в поток пользовательского интерфейса, чтобы отобразить изображение в интерфейсе.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

В этом сценарии существует немного неэффективности вызова StorageFile::OpenAsync. Требуется переключение контекста на фоновый поток, чтобы обработчик мог вернуть управление вызывающей стороне, после возобновления выполнения C++/WinRT восстанавливает контекст потока UI. Но в этом случае не обязательно находиться в потоке пользовательского интерфейса, пока мы не обновим пользовательский интерфейс. Чем больше API среда выполнения Windows мы вызываем перед вызовом winrt::resume_background, тем больше ненужных переключений контекста туда-сюда нам приходится выполнять. Решение состоит в том, чтобы не вызывать какие-либо API среда выполнения Windows до этого момента. Переместите их все после winrt::resume_background.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Если вы хотите сделать что-то более сложное, то можете написать собственные адаптеры для await. Например, если вы хотите, чтобы co_await возобновлялся в том же потоке, в котором завершается асинхронное действие (то есть без переключения контекста), можно было бы начать с написания адаптеров await, подобных приведённым ниже.

Note

Приведенный ниже пример кода предоставляется только для образовательных целей; Для начала вы узнаете, как работают адаптеры ожидания. Если вы хотите использовать эту технику в своей кодовой базе, мы рекомендуем вам разработать и протестировать собственные структуры-адаптеры await. Например, можно написать complete_on_any, complete_on_current, и complete_on(dispatcher). Кроме того, рекомендуется сделать их шаблонами, которые принимают тип IAsyncXxx в качестве параметра шаблона.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

Чтобы понять, как использовать await-адаптеры no_switch, сначала нужно знать, что, когда компилятор C++ встречает выражение co_await, он ищет функции с именами await_ready, await_suspend и await_resume. Библиотека C++/WinRT предоставляет эти функции, так что по умолчанию вы получаете разумное поведение, например, так.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Чтобы использовать адаптеры ожидания no_switch , просто измените тип этого co_await выражения с IAsyncXxx наno_switch, как показано ниже.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

Затем вместо поиска трех функций await_xxx , соответствующих IAsyncXxxx, компилятор C++ ищет функции, соответствующие no_switch.

Более глубокое погружение в winrt::resume_foreground

Начиная с версии C++/WinRT 2.0, функция winrt::resume_foreground выполняет приостановку даже в том случае, если она вызывается из потока диспетчера (в предыдущих версиях в некоторых сценариях это могло приводить к взаимоблокировкам, поскольку приостановка выполнялась только в том случае, если вызов происходил не из потока диспетчера).

Текущее поведение означает, что можно рассчитывать на то, что разворачивание стека и повторная постановка в очередь будут выполнены, а это важно для стабильности системы, особенно в низкоуровневом системном коде. Последнее описание кода в разделе Программирование с сопоставлением потоков, приведенное выше, иллюстрирует выполнение некоторых сложных вычислений в фоновом потоке, а затем переключение на соответствующий поток пользовательского интерфейса для обновления пользовательского интерфейса (пользовательского интерфейса).

Вот как winrt::resume_foreground выглядит внутренне.

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

Это текущее и предыдущее поведение аналогично разнице между PostMessage и SendMessage в разработке приложений Win32. PostMessage очереди работы, а затем распаковывает стек, не ожидая завершения работы. Раскрутка стека может быть важной.

Функция winrt::resume_foreground изначально поддерживала CoreDispatcher (привязанная к CoreWindow), которая была введена до Windows 10. В приложениях WinUI 3 и Windows App SDK используйте вместо этого DispatcherQueue. Вы можете создать dispatcherQueue для собственных целей. Рассмотрим это простое консольное приложение.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

В приведенном выше примере в отдельном потоке создается очередь (находящаяся внутри контроллера), а затем контроллер передается корутине. Корутин может использовать очередь для ожидания (приостановки и возобновления) в частном потоке. Еще одним распространенным способом использования DispatcherQueue является создание очереди в текущем потоке пользовательского интерфейса для традиционного классического приложения или приложения Win32.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

В этом примере показано, как вызывать и включать функции Win32 в проекты C++/WinRT, просто вызывая функцию CreateDispatcherQueueController в стиле Win32, чтобы создать контроллер, а затем передать владение результирующим контроллером очереди вызывающей функции в качестве объекта WinRT. Именно так также можно обеспечить эффективную и бесперебойную организацию очередей в вашем существующем классическом настольном Win32-приложении в стиле Пецольда.

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

Выше простая основная функция начинается с создания окна. Вы можете представить, что это регистрирует класс окна и вызывает CreateWindow , чтобы создать окно рабочего стола верхнего уровня. Затем вызывается функция CreateDispatcherQueueController для создания контроллера очереди, а затем — некоторой корутины с очередью диспетчера, принадлежащей этому контроллеру. Затем запускается традиционный цикл обработки сообщений, в котором возобновление сопрограммы естественным образом происходит в этом потоке. Выполнив это, вы можете вернуться в элегантный мир сопрограмм для асинхронных процессов или процессов обмена сообщениями в рамках вашего приложения.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

Вызов winrt::resume_foreground всегда будет ставить в очередь, а затем раскручивать стек вызовов. Вы также можете задать приоритет возобновления.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

Или, используя порядок очередей по умолчанию.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Note

Как показано выше, обязательно включите заголовок проекции для пространства имен типа, который вы используете co_await. Например, Windows::System::DispatcherQueue или Microsoft::UI::Dispatching::DispatcherQueue.

Или, в этом случае обнаружение завершения работы очереди и его правильная обработка.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

Выражение co_await возвращает true, указывая на то, что возобновление выполнения произойдёт в потоке диспетчера. Другими словами, постановка в очередь прошла успешно. В этом случае он возвращает значение false, указывающее, что выполнение продолжает выполняться в вызывающем потоке, поскольку контроллер очереди завершается и больше не обслуживает запросы к очереди.

Итак, если сочетать C++/WinRT с корутинами, в вашем распоряжении оказываются поистине огромные возможности — особенно при разработке классических настольных приложений в духе Петцольда.

Отмена асинхронной операции и функций обратного вызова при отмене

Функции среда выполнения Windows для асинхронного программирования позволяют отменить асинхронное действие или операцию в полете. Ниже приведен пример, который вызывает StorageFolder::GetFilesAsync для получения потенциально большой коллекции файлов, и он сохраняет результирующий объект асинхронной операции в элементе данных. Пользователь имеет возможность отменить операцию.

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

Что касается реализации отмены, начнем с простого примера.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancelationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

Если вы запустите приведенный выше пример, то увидите, что ImplicitCancelationAsync будет выводить по одному сообщению в секунду в течение трёх секунд, после чего автоматически завершится в результате отмены. Это работает, потому что при встрече с выражением co_await корутина проверяет, не была ли она отменена. Если это так, то срабатывает short-circuit; а если нет, то выполнение приостанавливается как обычно.

Отмена, конечно, может произойти в то время как корутина приостановлена. Только когда выполнение корутины возобновляется или она достигает другого co_await, будет выполняться проверка на отмену. Проблема является одной из потенциально слишком грубой задержки при реагировании на отмену.

Итак, ещё один вариант — явно проверять, не запрошена ли отмена, внутри вашей корутины. Обновите приведённый выше пример, используя код из листинга ниже. В этом новом примере ExplicitCancelationAsync получает объект, возвращаемый функцией winrt::get_cancellation_token, и использует его, чтобы периодически проверять, была ли отменена корутина. Пока сопрограмма не отменена, цикл выполняется бесконечно; после ее отмены цикл завершается, и функция завершает работу в обычном режиме. Результат такой же, как и в предыдущем примере, но в данном случае выход выполняется явно и контролируемо.

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

    while (!cancelation_token())
    {
        std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

При ожидании winrt::get_cancellation_token возвращается маркер отмены с информацией о IAsyncAction, который корутина создаёт от вашего имени. Оператор вызова функции можно использовать с этим токеном, чтобы проверить состояние отмены — по сути, это опрос состояния отмены. Если вы выполняете некоторую операцию с привязкой к вычислениям или выполняете итерацию через большую коллекцию, это разумный метод.

Регистрация обратного вызова отмены

Отмена в среда выполнения Windows не распространяется автоматически на другие асинхронные объекты. Однако, начиная с Windows SDK версии 10.0.17763.0 (Windows 10 версии 1809), можно зарегистрировать обратный вызов отмены. Это предварительный перехватчик, через который можно передавать сигнал отмены, что позволяет интегрироваться с существующими библиотеками параллелизма.

В следующем примере кода NestedCoroutineAsync выполняет работу, но в ней нет специальной логики отмены. CancelationPropagatorAsync по сути представляет собой обёртку вокруг вложенной корутины; эта обёртка заранее пробрасывает отмену.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancelation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancelationPropagatorAsync регистрирует лямбда-функцию для собственного обратного вызова отмены, а затем ожидает (приостанавливается) до завершения вложенных работ. Когда CancellationPropagatorAsync отменяется, он передаёт отмену вложенной корутине. Нет необходимости провести опрос по отмене; отмена не заблокирована на неопределенный срок. Этот механизм достаточно гибкий, чтобы использовать его для взаимодействия с библиотекой корутины или параллелизма, которая не знает ничего из C++/WinRT.

Отчет о ходе работы

Если ваша корутина возвращает либо IAsyncActionWithProgress, либо IAsyncOperationWithProgress, то можно получить объект, возвращаемый функцией winrt::get_progress_token, и использовать его для передачи сведений о ходе выполнения обработчику хода выполнения. Ниже приведен пример кода.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress.set_result(pi_so_far);
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress.set_result(pi_so_far);
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress.set_result(pi_so_far);
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress.set_result(pi_so_far);
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    progress(1.0);

    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& sender, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
                   << L"Value so far: " << sender.GetResults() << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

Чтобы сообщить о ходе выполнения, вызовите токен прогресса, передав значение прогресса в качестве аргумента. Чтобы задать предварительный результат, используйте метод set_result() в токене прогресса.

Note

Для передачи предварительных результатов требуется C++/WinRT версии 2.0.210309.3 или более поздней версии.

В приведенном выше примере показано, как задать предварительный результат для каждого отчета о ходе выполнения. Вы можете сообщить предварительные результаты в любое время, если вообще. Она не должна быть связана с отчетом о ходе выполнения.

Note

Некорректно реализовать несколько обработчиков завершения для асинхронного действия или операции. У вас может быть один делегат для завершенного события или его можно co_await . Если у вас есть оба, то второй завершится ошибкой. Подходит любой из следующих двух типов обработчиков завершения, но не оба для одного и того же асинхронного объекта.

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

Дополнительные сведения о обработчиках завершения см. в разделе "Типы делегатов" для асинхронных действий и операций.

Огонь и забудь

Иногда у вас есть задача, которую можно выполнять параллельно с другой работой, и вам не нужно дожидаться её завершения (никакая другая работа от неё не зависит), а также не нужно, чтобы она возвращала какое-либо значение. В таком случае можно просто запустить задачу и больше о ней не думать. Это можно сделать, написав сопрограмму, тип возврата которой — winrt::fire_and_forget (вместо одного из типов асинхронных операций среда выполнения Windows или concurrency::task).

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

winrt::fire_and_forget также полезен в качестве типа возвращаемого значения для обработчика событий, если в нём нужно выполнять асинхронные операции. Ниже приведен пример (см. также надежные и слабые ссылки в C++/WinRT).

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

Первый аргумент ( отправитель) остается без имени, так как мы никогда не используем его. По этой причине мы можем спокойно оставить это в качестве ориентира. Но обратите внимание, что args передается по значению. См. приведенный выше раздел "Передача параметров ".

Ожидается дескриптор ядра

C++/WinRT предоставляет функцию winrt::resume_on_signal , которую можно использовать для приостановки до сигнала события ядра. Вы отвечаете за то, чтобы дескриптор оставался действительным до возврата из co_await resume_on_signal(h). resume_on_signal сам по себе не может вам в этом помочь, так как вы, возможно, утратили дескриптор ещё до запуска resume_on_signal, как в этом первом примере.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

Переданный HANDLE действителен только до возврата из функции, а эта функция (являющаяся корутиной) возвращает управление в первой точке приостановки (в данном случае — в первой co_await). В ожидании DoWorkAsync управление вернулось вызывающему коду, вызывающий стековый кадр вышел из области видимости, и вы больше не знаете, будет ли дескриптор действителен, когда ваша сопрограмма возобновит выполнение.

Технически наша сопрограмма получает свои параметры по значению, как и должно быть (см. раздел Передача параметров выше). Но в этом случае нам нужно пойти ещё на шаг дальше, чтобы следовать духу этих рекомендаций, а не только их букве. Нам нужно передать сильную ссылку (другими словами, владение) вместе с дескриптором. Вот как.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

Передача winrt::handle по значению задаёт семантику владения, что гарантирует, что дескриптор ядра остаётся действительным на протяжении всего времени жизни корутины.

Вот как вы можете назвать этот корутин.

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
		        other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

Вы можете передать значение тайм-аута в resume_on_signal, как в этом примере.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

Асинхронные тайм-ауты — это просто

C++/WinRT в значительной степени опирается на корутины C++. Их влияние на разработку многопоточного кода поистине революционно. В этом разделе рассматриваются случаи, когда подробности асинхронности не важны и вам нужен только результат, сразу же. По этой причине в реализации интерфейса асинхронной операции среда выполнения Windows IAsyncAction в C++/WinRT предусмотрена функция get, аналогичная той, что предоставляет std::future.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

Функция get блокирует выполнение на неопределенный срок, до завершения асинхронного объекта. Асинхронные объекты, как правило, существуют очень недолго, поэтому зачастую это всё, что вам нужно.

Но есть случаи, когда это недостаточно, и вам нужно отказаться от ожидания через некоторое время. Написание этого кода всегда было возможным благодаря стандартным блокам, предоставляемым среда выполнения Windows. Но теперь C++/WinRT упрощает работу, предоставляя функцию wait_for . Это также поддерживается в IAsyncAction и, опять же, аналогично тому, что предоставляет std::future.

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

Note

wait_for использует std::chrono::duration в качестве типа в интерфейсе, но ограничен диапазоном, меньшим, чем допускает std::chrono::duration (примерно 49,7 дня).

В следующем примере wait_for ожидает около пяти секунд, а затем проверяет завершение. Если результат сравнения положительный, значит, асинхронный объект успешно завершился, и на этом всё. Если вы ожидаете некоторого результата, вы можете просто следовать этому с вызовом метода GetResults , чтобы получить результат.

Note

wait_for и get являются взаимоисключающими (нельзя вызвать оба метода). Каждый из них считается waiter, а асинхронные действия и операции среда выполнения Windows поддерживают только один waiter.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

Так как к тому времени асинхронный объект завершился, метод GetResults немедленно возвращает результат без дальнейшего ожидания. Как видно, wait_for возвращает состояние асинхронного объекта. Итак, вы можете использовать это для более тонкой настройки, например, вот так.

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • Помните, что AsyncStatus::Completed означает, что асинхронный объект успешно завершен, и вы можете вызвать метод GetResults для получения любого результата.
  • AsyncStatus::Canceled означает, что асинхронный объект был отменен. Обычно отмена запрашивается вызывающей стороной, поэтому это состояние редко требуется обрабатывать. Как правило, отмененный асинхронный объект просто удаляется. При желании можно вызвать метод GetResults, чтобы повторно вызвать исключение отмены.
  • AsyncStatus::Error означает, что в асинхронном объекте произошёл какой-либо сбой. При желании можно вызвать метод GetResults , чтобы повторно выполнить исключение.
  • AsyncStatus::Started означает, что асинхронный объект по-прежнему работает. Асинхронный шаблон среда выполнения Windows не допускает ни множественных ожиданий, ни нескольких ожидающих. Это означает, что нельзя вызывать wait_for в цикле. Если время ожидания фактически истекло, у вас остаётся несколько вариантов. Можно отказаться от объекта или провести опрос состояния перед вызовом метода GetResults для получения любого результата. Но на данном этапе лучше всего просто отбросить объект.

Альтернативный вариант — проверять только наличие Started и позволить GetResults обработать остальные случаи.

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

Асинхронный возврат массива

Ниже приведён пример MIDL 3.0, который вызывает ошибку MIDL2025: [msg]синтаксическая ошибка [контекст]: ожидается > или рядом с "[".

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

Причина заключается в том, что недопустимо использовать массив в качестве аргумента типа параметра для параметризованного интерфейса. Поэтому нам нужен менее очевидный способ достичь цели асинхронного передачи массива обратно из метода класса среды выполнения.

Можно вернуть массив, упакованный в объект PropertyValue. Вызывающий код затем распаковывает его. Вот пример кода, который можно опробовать, добавив класс среды выполнения SampleComponent в проект среда выполнения Windows Component (C++/WinRT), а затем использовать его, например, в проекте Blank App, Packaged (WinUI 3 in Desktop).

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

Важные API