Сложные сценарии с параллельной обработкой и асинхронными операциями в 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 должен обновляться из создавшего его потока, а это поток пользовательского интерфейса. Одним из решений является захват контекста потока, в котором изначально была вызвана наша соподпрограмма. Чтобы сделать это, следует создать экземпляр объекта 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 (обращаясь к его свойству Dispatcher). Реализация winrt::resume_foreground вызывает CoreDispatcher.RunAsync на этом объекте-диспетчере для выполнения работы, поступающей в дальнейшем в соподпрограмме.

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

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

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

Контексты выполнения, возобновление и переключение в соподпрограмме

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

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

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++/WinR, адаптеры предоставляет другая библиотека, и необходимо понять, что эти адаптеры делают с точки зрения возобновления и контекстов.

Чтобы свести к минимуму переключение контекста, можно использовать методы, которые уже были представлены в этом разделе. Рассмотрим ряд иллюстрации их применения. В следующем примере с псевдокодом мы покажем структуру обработчика событий, который вызывает 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->Dispatcher());

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

    // Display the image in the UI.
}

Этот сценарий несколько неэффективен с точки зрения вызова StorageFile::OpenAsync. При этом необходимо выполнить переключение контекста на фоновый поток (чтобы обработчик мог вернуть выполнение вызывающей стороне). После возобновления C++/WinRT восстанавливает контекст потока пользовательского интерфейса. Однако в данном случае необязательно быть в потоке пользовательского интерфейса, пока нет необходимости обновлять пользовательский интерфейс. Чем больше дополнительных 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->Dispatcher());

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

    // Display the image in the UI.
}

Если вы хотите сделать что-то более сложное, можете записать свои собственные адаптеры ожидания. Например, если вы хотите применить co_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;
};

Чтобы понять, как использовать параметры ожидания 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, соответствующих IAsyncXxx, компилятор 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. Сейчас используется более гибкий и эффективный диспетчер — 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);
    }
}

Выше выполнение простой функции main начинается с создания окна. Можно представить, что при этом регистрируется соответствующий класс окна и вызывается 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;
 
    ...
}

Примечание.

Как показано выше, обязательно включите заголовок проекции для пространства имен типа (co_await). Например, Windows::UI::Core::CoreDispatcher, 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 Windows::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();
}

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

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

Таким образом, другой вариант заключается в явном опросе для отмены из вашей корутины. Обновите приведенный выше пример указанным ниже кодом. В этом новом примере ЯвныйCancelationAsync извлекает объект, возвращаемый функцией 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 не выполняется автоматически в другие асинхронные объекты. Но — представлено в версии 10.0.17763.0 (Windows 10 версии 1809) пакета SDK для Windows— можно зарегистрировать обратный вызов отмены. Это препустимый перехватчик, с помощью которого можно распространить отмену, и позволяет интегрироваться с существующими библиотеками параллелизма.

В следующем примере кода 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 регистрирует лямбда-функцию для собственного обратного вызова отмены, а затем ожидает (приостанавливается) до завершения вложенных работ. Если отмена CancelPropagatorAsync отменена, она распространяет отмену в вложенную корутину. Нет необходимости провести опрос по отмене; отмена не блокируется на неопределенный срок. Этот механизм является достаточно гибким, чтобы использовать его для взаимодействия с библиотекой соподпрограмм или функций параллелизма, которая не содержит сведений о работе с 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();
}

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

Примечание.

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

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

Примечание.

Для асинхронного действия или операции нельзя реализовывать несколько обработчиков завершения. Вы может использовать только один делегат для события завершения или применить для него 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.
}

Первому аргументу (sender) имя не задано, так как он не используется. Поэтому мы можем оставить его в качестве ссылки. Но обратите внимание, что 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++/WinRT асинхронного интерфейса среды выполнения Windows IAsyncAction включает функцию get, которая соответствует функции, предоставляемой std::future.

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

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

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

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

Примечание.

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

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

Примечание.

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

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]syntax error [context]: expecting > or, near "[".

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

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

Вы можете вернуть массив, упакованный в объект PropertyValue. Затем вызывающий код его распакует. Ниже приведен пример кода, который можно попробовать применить, добавив класс среды выполнения SampleComponent в проект компонента среды выполнения Windows (C++/WinRT), а затем использовав его (например) из проекта приложения основных компонентов (C++/WinRT).

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