Simultaneidad y operaciones asincrónicas con C++/WinRT

Important

En este tema se presentan los conceptos de corrutinas y co_await, que se recomienda usar en la interfaz de usuario y en las aplicaciones que no son de interfaz de usuario. Para simplificar, la mayoría de los ejemplos de código de este tema introductorio muestran proyectos de aplicación de consola de Windows (C++/WinRT). Los ejemplos de código posteriores de esta sección sí usan corrutinas, pero, por comodidad, los ejemplos de aplicaciones de consola también siguen usando la llamada a la función de bloqueo get justo antes de finalizar, para que la aplicación no finalice antes de que termine de imprimir la salida. No harás eso (llamar a la función get bloqueante) desde un hilo de interfaz de usuario. En su lugar, usarás la instrucción co_await. Las técnicas que usará en las aplicaciones de interfaz de usuario se describen en el tema Simultaneidad avanzada y asincronía.

En este tema introductorio se muestran algunas de las formas en las que se pueden crear y consumir Windows Runtime objetos asincrónicos con C++/WinRT. Después de leer este tema, especialmente para las técnicas que usará en las aplicaciones de interfaz de usuario, consulte también Advanced concurrency and asynchrony (Simultaneidad avanzada y asincronía).

Operaciones asincrónicas y funciones "Async" de Windows Runtime

Cualquier API de Windows Runtime que tenga la posibilidad de tardar más de 50 milisegundos en completarse se implementa como una función asincrónica (con un nombre que termina en "Async"). La implementación de una función asincrónica inicia el trabajo en otro subproceso y devuelve inmediatamente con un objeto que representa la operación asincrónica. Cuando se completa la operación asincrónica, ese objeto devuelto contiene cualquier valor resultante del trabajo. El espacio de nombres Windows::Foundation Windows Runtime contiene cuatro tipos de objeto de operación asincrónica.

Cada uno de estos tipos de operaciones asincrónicas se proyecta en un tipo correspondiente en el espacio de nombres winrt::Windows::Foundation C++/WinRT. C++/WinRT también contiene una estructura interna del adaptador await. No lo usa directamente, pero, gracias a esa estructura, puede escribir una co_await instrucción para esperar de forma cooperativa el resultado de cualquier función que devuelva uno de estos tipos de operación asincrónica. Y puede crear sus propias corrutinas que devuelvan estos tipos.

Un ejemplo de una función de Windows asincrónica es SyndicationClient::RetrieveFeedAsync, que devuelve un objeto de operación asincrónica de tipo IAsyncOperationWithProgress<TResult, TProgress>.

Echemos un vistazo a algunas maneras (primero bloqueando y, a continuación, sin bloqueo) de usar C++/WinRT para llamar a una API como esa. Solo para ilustrar las ideas básicas, usaremos un proyecto de aplicación de consola de Windows (C++/WinRT) en los ejemplos de código siguientes. Las técnicas más adecuadas para una aplicación de interfaz de usuario se describen en Simultaneidad avanzada y asincronía.

Bloquear el hilo llamante

El ejemplo de código siguiente recibe un objeto de operación asincrónica de RetrieveFeedAsync y llama a get en ese objeto para bloquear el subproceso que realiza la llamada hasta que los resultados de la operación asincrónica estén disponibles.

Si desea copiar y pegar este ejemplo directamente en el archivo de código fuente principal de un proyecto de aplicación de consola de Windows (C++/WinRT), establezca primero No usar encabezados precompilados en las propiedades del proyecto.

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

Llamar a get facilita la programación y es ideal para aplicaciones de consola o hilos en segundo plano cuando quizá no quiera usar una corrutina por la razón que sea. Pero no es concurrente ni asíncrono, por lo que no es adecuado para un hilo de interfaz de usuario (y saltará una aserción en compilaciones no optimizadas si intenta usarlo en él). Para evitar que los subprocesos del sistema operativo realicen otro trabajo útil, necesitamos una técnica diferente.

Escribir una corrutina

C++/WinRT integra corrutinas de C++ en el modelo de programación para proporcionar una manera natural de esperar de forma cooperativa un resultado. Puede crear su propia operación asíncrona de Windows Runtime escribiendo una corrutina. En el ejemplo de código siguiente, ProcessFeedAsync es la corrutina.

Note

La función get existe en el tipo de proyección de C++/WinRT winrt::Windows::Foundation::IAsyncAction, por lo que puede llamar a la función desde cualquier proyecto de C++/WinRT. No encontrará la función enumerada como miembro de la interfaz IAsyncAction, ya que get no forma parte de la superficie de la interfaz binaria de la aplicación (ABI) del tipo Windows Runtime real 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.
}

Una corrutina es una función que se puede suspender y reanudar. En la corrutina ProcessFeedAsync anterior, cuando se alcanza la instrucción co_await, la corrutina inicia de forma asíncrona la llamada a RetrieveFeedAsync e inmediatamente se suspende y devuelve el control al código que la llamó (que es main en el ejemplo anterior). main puede seguir funcionando mientras se recupera e imprime la fuente. Cuando eso haya terminado (cuando se complete la llamada a RetrieveFeedAsync), la corrutina ProcessFeedAsync se reanuda en la instrucción siguiente.

Puede agrupar una corrutina con otras corrutinas. O puede llamar a get para bloquearse y esperar a que se complete (y obtener el resultado, si lo hay). O bien, puede pasarlo a otro lenguaje de programación que admita el Windows Runtime.

También es posible manejar los eventos de finalización y/o de progreso de acciones y operaciones asíncronas mediante delegados. Para obtener más información y ejemplos de código, consulte Tipos delegados para acciones y operaciones asincrónicas.

Como puede ver, en el ejemplo de código anterior, seguimos usando la llamada de función get de bloqueo justo antes de salir de main. Pero eso es solo para que la aplicación no se cierre antes de que termine de imprimir la salida.

Devolver de forma asincrónica un tipo de Windows Runtime

En este ejemplo siguiente encapsulamos una llamada a RetrieveFeedAsync, para un URI específico, para proporcionarnos una función RetrieveBlogFeedAsync que devuelve de forma asincrónica un 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());
}

En el ejemplo anterior, RetrieveBlogFeedAsync devuelve un IAsyncOperationWithProgress, que tiene tanto progreso como un valor devuelto. Podemos realizar otras tareas mientras RetrieveBlogFeedAsync hace su trabajo y recupera el canal. A continuación, llamamos a get sobre ese objeto de operación asincrónica para bloquear la ejecución, esperar a que se complete y, a continuación, obtener los resultados de la operación.

Si devuelve de forma asincrónica un tipo de Windows Runtime, debe devolver un IAsyncOperation<TResult> o un IAsyncOperationWithProgress<TResult, TProgress>. Cualquier clase en tiempo de ejecución propia o de terceros es válida, así como cualquier tipo que se pueda pasar a una función de Windows Runtime o devolver desde ella (por ejemplo, int, o winrt::hstring). El compilador mostrará el error "T must be WinRT type" si intentas usar uno de estos tipos de operaciones asincrónicas con un tipo que no sea de Windows Runtime.

Si una corrutina no tiene al menos una instrucción co_await, entonces, para considerarse una corrutina, debe tener al menos una instrucción co_return o una instrucción co_yield. Habrá casos en los que la corrutina pueda devolver un valor sin introducir ninguna asincronía y, por tanto, sin bloquear ni cambiar el contexto. Aquí tienes un ejemplo que hace eso (la segunda vez que se llama, y las siguientes) mediante el almacenamiento en caché de un valor.

winrt::hstring m_cache;

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

Devuelve asincrónicamente un tipo que no sea de Windows Runtime

Si devuelve de forma asincrónica un tipo que no es un tipo de Windows Runtime, debe devolver una concurrency::task de la Biblioteca de Patrones Paralelos (PPL). Recomendamos concurrency::task porque ofrece un mejor rendimiento (y una mejor compatibilidad de cara al futuro) que std::future.

Tip

Si incluye <pplawait.h>, puede usar concurrency::task como tipo de corrutina.

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

Paso de parámetros

Para las funciones sincrónicas, debería usar parámetros const& por defecto. Esto evitará la sobrecarga de las copias (que implican el recuento de referencias y eso significa incrementos interbloqueados y decrementos).

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

Pero puede encontrarse con problemas si pasa un parámetro de referencia a una corrutina.

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

En una corrutina, la ejecución es sincrónica hasta el primer punto de suspensión, donde el control se devuelve al autor de la llamada y el marco de llamada sale del ámbito. A la hora en que se reanuda la corrutina, es posible que todo haya ocurrido con el valor de origen al que hace referencia un parámetro de referencia. Desde la perspectiva de la corrutina, un parámetro de referencia tiene una duración no controlada. Por lo tanto, en el ejemplo anterior, estamos seguros de acceder al valor hasta co_await, pero no después de él. En caso de que la función que llama destruya value, intentar acceder a él dentro de la corrutina después provoca corrupción de memoria. Tampoco podemos pasar de forma segura el valor a DoOtherWorkAsync si hay algún riesgo de que esa función se suspenda a su vez e intente usar el valor después de que se reanude.

Para que los parámetros sean seguros para su uso después de suspender y reanudar, las corrutinas deben usar el paso por valor por defecto para garantizar que estas capturen por valor y evitar problemas de tiempo de vida. Los casos en los que puede desviarse de esa guía porque está seguro de que es seguro hacerlo va a ser poco frecuente.

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

Pasar por valor requiere que el argumento sea barato de mover o copiar, y eso suele cumplirse en el caso de un puntero inteligente.

También se puede defender que, a menos que se quiera mover el valor, pasarlo por valor constante es una buena práctica. No afectará al valor original del que está copiando, pero deja clara la intención y resulta útil si modifica la copia sin querer.

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

Consulte también Matrices y vectores estándar, que trata sobre cómo pasar un vector estándar a un destinatario asincrónico.

Si no puede cambiar la firma de la corrutina, pero puede cambiar la implementación, puede realizar una copia local antes del primer 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).
}

Si Param es costoso copiar, extraiga solo las piezas que necesita antes del primer 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).
}

Acceso seguro al puntero this en una corrutina miembro de clase

Consulta Referencias fuertes y débiles en C++/WinRT.

API importantes