Параллелизм и асинхронные операции с C++/WinRT

Это важно

В этом разделе представлены понятия сопрограмм и co_await, которые мы рекомендуем использовать как в вашем UI, так и в приложениях без пользовательского интерфейса. Для простоты большинство примеров кода в этом вводном разделе показывают проекты Windows консольного приложения (C++/WinRT). В последующих примерах кода в этой теме действительно используются корутины, но для удобства в примерах консольного приложения также по-прежнему используется блокирующий вызов функции get непосредственно перед завершением работы, чтобы приложение не завершилось до того, как закончит вывод своих данных. Вы не будете этого делать (вызывать блокирующую функцию get) в потоке пользовательского интерфейса. Вместо этого вы будете использовать инструкцию co_await . Приёмы, которые вы будете использовать в приложениях с пользовательским интерфейсом, описаны в разделе Расширенные возможности параллелизма и асинхронности.

В этой вводной теме описаны некоторые способы, с помощью которых можно как создавать асинхронные объекты среда выполнения Windows, так и работать с ними, используя C++/WinRT. Ознакомившись с этим разделом, особенно если вы будете использовать описанные в нём методы в приложениях пользовательского интерфейса, см. также раздел Расширенные сведения о параллелизме и асинхронности.

Асинхронные операции и функции "Async" среды среда выполнения Windows

Любой API среда выполнения Windows, выполнение которого может занять более 50 миллисекунд, реализован в виде асинхронной функции (с именем, заканчивающимся на "Async"). Реализация асинхронной функции инициирует работу в другом потоке и возвращает немедленно объект, представляющий асинхронную операцию. Когда асинхронная операция завершается, возвращенный объект содержит значение, полученное в результате ее выполнения. Пространство имен Windows::Foundation среда выполнения Windows содержит четыре типа объекта асинхронной операции.

Каждый из этих асинхронных типов операций проецируется в соответствующий тип в пространстве имен winrt::Windows::Foundation C++/WinRT. C++/WinRT также содержит структуру внутреннего адаптера await. Вы не используете его напрямую, но, благодаря этой структуре, вы можете написать co_await инструкцию для совместного ожидания результата любой функции, которая возвращает один из этих типов асинхронных операций. И вы можете создать собственные корутины, которые возвращают эти типы.

Пример асинхронной функции Windows — SyndicationClient::RetrieveFeedAsync, который возвращает объект асинхронной операции типа IAsyncOperationWithProgress TResult, TProgress<.>

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

Блокировать вызывающий поток

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

Если вы хотите скопировать этот пример непосредственно в основной файл исходного кода проекта Windows консольного приложения (C++/WinRT), сначала задайте параметр "Не использовать предварительно скомпилированные заголовки" в свойствах проекта.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

Вызов get удобен при написании кода и отлично подходит для консольных приложений или фоновых потоков, где по тем или иным причинам вы, возможно, не захотите использовать корутину. Но оно не поддерживает ни конкурентное, ни асинхронное выполнение, поэтому не подходит для потока пользовательского интерфейса (и в неоптимизированных сборках сработает assert, если вы попытаетесь использовать его в таком потоке). Чтобы не занимать потоки ОС и дать им выполнять другую полезную работу, нам нужен другой способ.

Написание корутины

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

Note

Функция get существует в типе проекции C++/WinRT winrt::Windows::Foundation::IAsyncAction, чтобы можно было вызвать функцию из любого проекта C++/WinRT. Вы не найдете функцию, указанную в качестве члена интерфейса IAsyncAction, так как get не является частью поверхности двоичного интерфейса приложения (ABI) фактического типа IAsyncAction типа среда выполнения Windows.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

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

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

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

Вы можете агрегировать корутин в другие корутины. Или вы можете вызвать get, чтобы заблокировать выполнение и дождаться завершения (и получить результат, если он есть). Или вы можете передать его другому языку программирования, который поддерживает среда выполнения Windows.

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

Как видно, в приведённом выше примере кода мы по-прежнему используем блокирующий вызов функции get непосредственно перед выходом из main. Но это только так, чтобы приложение не выходило до завершения печати выходных данных.

Асинхронно возвращает тип среда выполнения Windows

В следующем примере мы обернём вызов RetrieveFeedAsync для определённого URI, чтобы получить функцию RetrieveBlogFeedAsync, которая асинхронно возвращает SyndicationFeed.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

В приведенном выше примере RetrieveBlogFeedAsync возвращает IAsyncOperationWithProgress, который имеет как ход выполнения, так и возвращаемое значение. Мы можем заниматься другими делами, пока RetrieveBlogFeedAsync работает и получает ленту. Затем мы вызываем get для этого объекта асинхронной операции, чтобы заблокировать выполнение, дождаться его завершения, а затем получить результаты операции.

Если вы асинхронно возвращаете тип среда выполнения Windows, следует вернуть IAsyncOperation<TResult> или IAsyncOperationWithProgress<TResult, TProgress>. Любой класс среды выполнения, как собственный, так и сторонний, а также любой тип, который можно передавать в функцию среда выполнения Windows и получать из нее (например, int, или winrt::hstring). Компилятор выдаст сообщение об ошибке "T должен быть типом WinRT", если вы попытаетесь использовать один из этих типов асинхронных операций с типом, не относящимся к среда выполнения Windows.

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

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

Асинхронно возвращает тип, отличный отWindows-Runtime

Если вы асинхронно возвращаете тип, не являющийся типом среда выполнения Windows, то следует возвращать concurrency::task из библиотеки параллельных шаблонов (PPL). Мы рекомендуем concurrency::task, поскольку она обеспечивает лучшую производительность (и в дальнейшем лучшую совместимость), чем std::future.

Подсказка

Если включить <pplawait.h>, то можно использовать concurrency::task как тип корутины.

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

Передача параметров

Для синхронных функций следует использовать const& параметры по умолчанию. Это позволит избежать издержек, связанных с копированием (которое включает подсчёт ссылок, а значит — атомарные операции инкремента и декремента).

// Synchronous function.
void DoWork(Param const& value);

Но могут возникнуть проблемы, если передать ссылочный параметр корутине.

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

В корутине выполнение происходит синхронно вплоть до первой точки приостановки, в которой управление возвращается вызывающей стороне, а стековый кадр вызывающей стороны выходит из области видимости. К моменту, когда корутина возобновит выполнение, с исходным значением, на которое ссылается ссылочный параметр, могло произойти что угодно. С точки зрения корутины, эталонный параметр имеет неконтролируемое время существования. Таким образом, в приведённом выше примере мы можем безопасно обращаться к значению вплоть до co_await, но не после него. Если value уничтожается вызывающей стороной, то последующая попытка доступа к нему внутри корутины приводит к повреждению памяти. Мы также не можем безопасно передать значение в DoOtherWorkAsync, если есть хоть какой-то риск, что эта функция, в свою очередь, приостановит выполнение, а затем попытается использовать значение после его возобновления.

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

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

Передача по значению требует, чтобы аргумент был недорогим для перемещения или копирования; и это обычно относится к интеллектуальному указателю.

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

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

См. также Стандартные массивы и векторы, где рассматривается, как передать стандартный вектор в асинхронный вызываемый объект.

Если вы не можете изменить сигнатуру корутины, но можете изменить её реализацию, то можете сделать локальную копию перед первым co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Если копирование Param обходится дорого, извлеките только те части, которые нужны, до первого co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

Безопасный доступ к этому указателю в соутине класса-члена

См. надежные и слабые ссылки в C++/WinRT.

Важные API