Параллельные обработка и выполнение асинхронных операций с помощью C++/WinRTConcurrency and asynchronous operations with C++/WinRT

Важно!

В этой статье содержится описание соподпрограмм и оператора co_await, которые рекомендуется использовать в приложениях с пользовательским интерфейсом и без него.This topic introduces the concepts of coroutines and co_await, which we recommend that you use in both your UI and in your non-UI applications. Для простоты большинство примеров кода в этой ознакомительной статье связаны с проектом консольного приложения для Windows (C++/WinRT) .For simplicity, most of the code examples in this introductory topic show Windows Console Application (C++/WinRT) projects. В примерах кода, приведенных в этой статье, используются соподпрограммы, но для удобства в примерах консольного приложения мы продолжим использовать вызов блокирующей функции get прямо перед выходом, чтобы приложение не завершало работу до окончания печати выходных данных.The later code examples in this topic do use coroutines, but for convenience the console application examples also continue to use the blocking get function call just before exiting, so that the application doesn't exit before finishing printing its output. Вы не будете вызывать блокирующую функцию get из потока пользовательского интерфейса.You won't do that (call the blocking get function) from a UI thread. Вместо этого будет использоваться оператор co_await.Instead, you'll use the co_await statement. Описание методов, которые вы будете использовать в приложениях пользовательского интерфейса, см. в статье Более сложные сценарии с параллельной обработкой и асинхронными операциями в C++/WinRT.The techniques that you'll use in your UI applications are described in the topic More advanced concurrency and asynchrony.

В этой ознакомительной статье показаны некоторые способы создания и использования асинхронных объектов среды выполнения Windows с помощью C++/WinRT.This introductory topic shows some of the ways in which you can both create and consume Windows Runtime asynchronous objects with C++/WinRT. После прочтения этой статьи, в частности описания методов, которые вы будете использовать в приложениях пользовательского интерфейса, ознакомьтесь со статьей Более сложные сценарии с параллельной обработкой и асинхронными операциями в C++/WinRT.After reading this topic, especially for techniques you'll use in your UI applications, also see More advanced concurrency and asynchrony.

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

Любой API-интерфейс среды выполнения Windows, для выполнения которого может потребоваться более 50 миллисекунд, реализуется как асинхронная функция (с именем, оканчивающимся на "Async").Any Windows Runtime API that has the potential to take more than 50 milliseconds to complete is implemented as an asynchronous function (with a name ending in "Async"). Реализация асинхронной функции инициирует работу в другом потоке и немедленно возвращает объект, представляющий асинхронную операцию.The implementation of an asynchronous function initiates the work on another thread, and returns immediately with an object that represents the asynchronous operation. После выполнения асинхронной операции возвращаемый объект, содержит любое значение, которое является результатом работы.When the asynchronous operation completes, that returned object contains any value that resulted from the work. Пространство имен среды выполнения Windows Windows::Foundation содержит четыре типа объектов асинхронной операции.The Windows::Foundation Windows Runtime namespace contains four types of asynchronous operation object.

Каждый из этих типов асинхронной операции проецируется в соответствующий тип в пространстве имен C++/WinRT winrt::Windows::Foundation.Each of these asynchronous operation types is projected into a corresponding type in the winrt::Windows::Foundation C++/WinRT namespace. C++/WinRT также содержит внутреннюю структуру адаптера ожидания.C++/WinRT also contains an internal await adapter struct. Она не используется напрямую, но благодаря этой структуре вы можете добавить оператор co_await для совместного ожидания результата любой функции, которая возвращает один из этих типов асинхронной операции.You don't use it directly but, thanks to that struct, you can write a co_await statement to cooperatively await the result of any function that returns one of these asynchronous operation types. И вы можете создавать свои собственные соподпрограммы, возвращающие эти типы.And you can author your own coroutines that return these types.

Пример асинхронной функции Windows — функция SyndicationClient::RetrieveFeedAsync, которая возвращает объект асинхронной операции типа IAsyncOperationWithProgress<TResult, TProgress>.An example of an asynchronous Windows function is SyndicationClient::RetrieveFeedAsync, which returns an asynchronous operation object of type IAsyncOperationWithProgress<TResult, TProgress>.

Рассмотрим сперва некоторые —блокирующие, а затем не блокирующие— способы использования C++/WinRT для вызова подобного API.Let's look at some ways—first blocking, and then non-blocking—of using C++/WinRT to call an API such as that. Для иллюстрации основных идей в следующих нескольких примерах кода мы будем использовать проект консольного приложения для Windows (C++/WinRT) .Just for illustration of the basic ideas, we'll be using a Windows Console Application (C++/WinRT) project in the next few code examples. Методы, более подходящие для приложения пользовательского интерфейса, описаны в статье Более сложные сценарии с параллельной обработкой и асинхронными операциями в C++/WinRT.Techniques that are more appropriate for a UI application are discussed in More advanced concurrency and asynchrony.

Блокировка вызывающего потокаBlock the calling thread

Приведенный ниже пример кода получает объект асинхронной операции от RetrieveFeedAsync и вызывает get для этого объекта, чтобы заблокировать вызывающий поток до тех пор, пока не будут доступны результаты выполнения асинхронной операции.The code example below receives an asynchronous operation object from RetrieveFeedAsync, and it calls get on that object to block the calling thread until the results of the asynchronous operation are available.

Если вы хотите скопировать и вставить этот пример непосредственно в главный файл исходного кода проекта консольного приложения Windows (C++/WinRT), сначала задайте параметр Не использовать предварительно скомпилированные заголовки в свойствах проекта.If you want to copy-paste this example directly into the main source code file of a Windows Console Application (C++/WinRT) project, then first set Not Using Precompiled Headers in project properties.

// 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 обеспечивает удобство написания кода и идеально подходит для консольных приложений, а также фоновых потоков, в которых по какой-либо причине не следует использовать соподпрограмму.Calling get makes for convenient coding, and it's ideal for console apps or background threads where you may not want to use a coroutine for whatever reason. Однако он не является ни одновременным, ни асинхронным, поэтому не подходит для потока пользовательского интерфейса (и в неоптимизированных сборках при попытке его использования будет выдано утверждение).But it's not concurrent nor asynchronous, so it's not appropriate for a UI thread (and an assertion will fire in unoptimized builds if you attempt to use it on one). Чтобы потоки операционной системы могли выполнять другую полезную работу, требуется иной способ.To avoid holding up OS threads from doing other useful work, we need a different technique.

Написание соподпрограммыWrite a coroutine

C++/WinRT интегрирует соподпрограммы C++ в модель программирования для обеспечения естественного способа совместного ожидания результата.C++/WinRT integrates C++ coroutines into the programming model to provide a natural way to cooperatively wait for a result. Вы можете создать свою собственную асинхронную операцию среды выполнения Windows путем написания соподпрограммы.You can produce your own Windows Runtime asynchronous operation by writing a coroutine. В следующем примере кода ProcessFeedAsync является соподпрограммой.In the code example below, ProcessFeedAsync is the coroutine.

Примечание

Функция get существует в типе проекции C++/WinRT winrt::Windows::Foundation::IAsyncAction, так что ее можно вызвать в любом проекте C++/WinRT.The get function exists on the C++/WinRT projection type winrt::Windows::Foundation::IAsyncAction, so you can call the function from within any C++/WinRT project. Эта функция отсутствует в перечне элементов интерфейса IAsyncAction, поскольку get не является частью области двоичного интерфейса приложения фактического типа среды выполнения Windows IAsyncAction.You will not find the function listed as a member of the IAsyncAction interface, because get is not part of the application binary interface (ABI) surface of the actual Windows Runtime type IAsyncAction.

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

Соподпрограмма — это функция с возможностью приостановки и возобновления.A coroutine is a function that can be suspended and resumed. В соподпрограмме ProcessFeedAsync, представленной выше, при достижении оператора co_await соподпрограмма асинхронно инициирует вызов RetrieveFeedAsync, а затем немедленно приостанавливает себя и возвращает управление вызывающему объекту (которым в примере выше является main).In the ProcessFeedAsync coroutine above, when the co_await statement is reached, the coroutine asynchronously initiates the RetrieveFeedAsync call and then it immediately suspends itself and returns control back to the caller (which is main in the example above). Затем main может продолжить работать, пока происходит извлечение и печать веб-канала.main can then continue to do work while the feed is being retrieved and printed. Когда это действие будет выполнено (после завершения вызова RetrieveFeedAsync), соподпрограмма ProcessFeedAsync возобновит работу на следующем операторе.When that's done (when the RetrieveFeedAsync call completes), the ProcessFeedAsync coroutine resumes at the next statement.

Соподпрограммы можно объединять в другие соподпрограммы.You can aggregate a coroutine into other coroutines. Или можно вызвать get для блокировки и дождаться завершения (а также получить результат, если таковой имеется).Or you can call get to block and wait for it to complete (and get the result if there is one). Кроме того, можно передать соподпрограмму в другой язык программирования, который поддерживает среду выполнения Windows.Or you can pass it to another programming language that supports the Windows Runtime.

Также можно обрабатывать завершенные и (или) текущие события из асинхронных действий и операций с помощью делегатов.It's also possible to handle the completed and/or progress events of asynchronous actions and operations by using delegates. Подробные сведения и примеры кода см. в разделе Типы делегатов для асинхронных действий и операций.For details, and code examples, see Delegate types for asynchronous actions and operations.

Как видно, в приведенном выше примере кода мы продолжаем использовать вызов блокирующей функции get непосредственно перед выходом из main.As you can see, in the code example above, we continue to use the blocking get function call just before exiting main. Но это необходимо лишь для того, чтобы приложение не завершило работу до окончания печати выходных данных.But that's only so that the application doesn't exit before finishing printing its output.

Асинхронное возвращение типа среды выполнения WindowsAsynchronously return a Windows Runtime type

В следующем примере мы создаем оболочку вызова RetrieveFeedAsync для определенного URI, чтобы получить функцию RetrieveBlogFeedAsync, которая асинхронно возвращает **SyndicationFeed **.In this next example we wrap a call to RetrieveFeedAsync, for a specific URI, to give us a RetrieveBlogFeedAsync function that asynchronously returns a 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 с данными о ходе выполнения и возвращаемым значением.In the example above, RetrieveBlogFeedAsync returns an IAsyncOperationWithProgress, which has both progress and a return value. Мы можем выполнять другую работу пока RetrieveBlogFeedAsync выполняет свою и получает канал.We can do other work while RetrieveBlogFeedAsync is doing its thing and retrieving the feed. После этого мы вызываем get для этого объекта асинхронной операции с целью блокировки, дожидаемся его завершения, а затем получаем результаты операции.Then, we call get on that asynchronous operation object to block, wait for it to complete, and then obtain the results of the operation.

Если вы асинхронно возвращаете тип среды выполнения Windows, то вам необходимо возвращать IAsyncOperation<TResult> или IAsyncOperationWithProgress<TResult, TProgress>.If you're asynchronously returning a Windows Runtime type, then you should return an IAsyncOperation<TResult> or an IAsyncOperationWithProgress<TResult, TProgress>. Для этого подходит любой класс среды выполнения собственной или сторонней разработки, а также любой тип, который может передаваться в функцию среды выполнения Windows или из нее (например, int или winrt::hstring).Any first- or third-party runtime class qualifies, or any type that can be passed to or from a Windows Runtime function (for example, int, or winrt::hstring). Компилятор поможет вам, отобразив ошибку "must be WinRT type" ("Требуется тип WinRT"), если вы попытаетесь использовать один из этих типов асинхронных операций с типом, который не является типом среды выполнения Windows.The compiler will help you with a "must be WinRT type" error if you try to use one of these asynchronous operation types with a non-Windows Runtime type.

Если в соподпрограмме нет хотя бы одного оператора co_await, чтобы считаться соподпрограммой, в ней должен быть хотя бы один оператор co_return или co_yield.If a coroutine doesn't have at least one co_await statement then, in order to qualify as a coroutine, it must have at least one co_return or one co_yield statement. При этом будут возникать ситуации, в которых ваша соподпрограмма может возвращать значение без добавления какой-либо асинхронности и, соответственно, без блокировки и переключения контекста.There will be cases where your coroutine can return a value without introducing any asynchrony, and therefore without blocking nor switching context. Вот пример, в котором это реализовано (при втором и последующих вызовах) за счет кэширования значения.Here's an example that does that (the second and subsequent times it's called) by caching a value.

winrt::hstring m_cache;

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

Асинхронное возвращение типа, не являющегося типом среды выполнения WindowsAsynchronously return a non-Windows-Runtime type

Если вы асинхронно возвращаете тип, который не является типом среды выполнения Windows, вам необходимо возвращать класс concurrency::task из библиотеки параллельных шаблонов (PPL).If you're asynchronously returning a type that's not a Windows Runtime type, then you should return a Parallel Patterns Library (PPL) concurrency::task. Мы рекомендуем concurrency::task, поскольку он обеспечивает более высокую производительность (и в дальнейшем улучшенную совместимость), чем std::future.We recommend concurrency::task because it gives you better performance (and better compatibility going forward) than std::future does.

Совет

Если включить <pplawait.h>, то вы можете использовать concurrency::task в качестве типа соподпрограммы.If you include <pplawait.h>, then you can use concurrency::task as a coroutine type.

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

Передача параметровParameter-passing

Для синхронных функций следует использовать заданные по умолчанию параметры const&.For synchronous functions, you should use const& parameters by default. Это позволит избежать дополнительной нагрузки, связанной с копиями (в частности подсчета ссылок, предполагающего непрерывающееся выполнение операций увеличения и уменьшения).That will avoid the overhead of copies (which involve reference counting, and that means interlocked increments and decrements).

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

Однако здесь могут возникать проблемы при передаче параметра ссылки соподпрограмме.But you can run into problems if you pass a reference parameter to a coroutine.

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

В сопрограмме выполнение происходит синхронно до первой точки приостановки, где управление возвращается вызывающему объекту, и соответствующий фрейм больше не используется.In a coroutine, execution is synchronous up until the first suspension point, where control is returned to the caller and the calling frame goes out of scope. К моменту возобновления соподпрограммы с исходным значением, на которое ссылается отсылочный параметр, может случиться что угодно.By the time the coroutine resumes, anything might have happened to the source value that a reference parameter references. С точки зрения соподпрограммы у отсылочного параметра неконтролируемый срок существования.From the coroutine's perspective, a reference parameter has uncontrolled lifetime. Таким образом, в приведенном выше примере мы безопасно можем получить доступ к value до вызова co_await, но не после него.So, in the example above, we're safe to access value up until the co_await, but not after it. Если value уничтожается вызывающим объектом, пытающимся получить доступ к внутренней сопрограмме, это приводит к повреждению памяти.In the event that value is destructed by the caller, attempting to access it inside the coroutine after that results in a memory corruption. Мы также не можем безопасно передать value в DoOtherWorkAsync, если есть малейший риск того, что эта функция в свою очередь приостановится, а затем попытается использовать value после возобновления.Nor can we safely pass value to DoOtherWorkAsync if there's any risk that that function will in turn suspend and then try to use value after it resumes.

Чтобы обеспечить безопасное использование параметров после приостановки и возобновления, сопрограммы должны по умолчанию использовать передачу по значению. Так данные будут собираться только по значению, предотвращая появление проблем с временем существования.To make parameters safe to use after suspending and resuming, your coroutines should use pass-by-value by default to ensure that they capture by value, and avoid lifetime issues. Ситуации, в которых можно не соблюдать эти указания в связи с уверенностью в безопасности такого подхода, встречаются редко.Cases when you can deviate from that guidance because you're certain that it's safe to do so are going to be rare.

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

При передаче по значению требуется, чтобы аргумент было несложно перемещать или копировать, что характерно для смарт-указателя.Passing by value requires that the argument be inexpensive to move or copy; and that's typically the case for a smart pointer.

Кроме того, не рекомендуется использовать передачу по значению const (если вы не собираетесь переместить значение).It's also arguable that (unless you want to move the value) passing by const value is good practice. Это никак не влияет на исходное значение, копию которого вы создаете, но явно сообщает намерение и помогает в случае неумышленного изменения копии.It won't have any effect on the source value from which you're making a copy, but it makes the intent clear, and helps if you inadvertently modify the copy.

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

С дополнительными сведениями можно также ознакомиться в разделе Стандартные массивы и векторы, в котором описываются методы передачи стандартного вектора в асинхронный вызываемый элемент.Also see Standard arrays and vectors, which deals with how to pass a standard vector into an asynchronous callee.

Если нельзя изменить подпись сопрограммы, но можно изменить реализацию, создайте локальную копию перед первым экземпляром co_await.If you can't change your coroutine's signature, but you can change the implementation, then you can make a local copy before the first 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.If Param is expensive to copy, then extract just the pieces you need before the first 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).
}

Безопасный доступ к указателю this в сопрограмме членов классаSafely accessing the this pointer in a class-member coroutine

См. статью Сильные и слабые ссылки в C++/WinRT.See Strong and weak references in C++/WinRT.

Важные APIImportant APIs