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). En los ejemplos de código últimos de este tema se usan corrutinas, pero, para mayor comodidad, los ejemplos de aplicación de consola también siguen usando la llamada de función de bloqueo get justo antes de salir, de modo que la aplicación no sale antes de finalizar la impresión de la salida. Tú no hará eso (llamar a la función de bloqueo get) desde un subproceso de UI. 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 puedes crear tus 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();
}

Una llamada a get ofrece una codificación cómoda y es ideal para aplicaciones de consola o subprocesos en segundo plano donde no quieres usar una corrutina por el motivo que sea. Pero no es simultánea ni asincrónica, por lo que no es apropiada para un subproceso de interfaz de usuario (y se activará una aserción en compilaciones no optimizadas si intentas usarla en una). 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. Puedes producir tu propia operación asincrónica de Windows Runtime escribiendo un 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 anterior corrutina ProcessFeedAsync, cuando se alcanza la instrucción co_await, la corrutina inicia de forma asincrónica la llamada RetrieveFeedAsync, después se suspende inmediatamente y devuelve el control al autor de la llamada (que es main en el ejemplo anterior). main puede seguir funcionando mientras la fuente se recupera y se imprime. Una vez hecho esto (cuando se completa la llamada RetrieveFeedAsync), la corrutina ProcessFeedAsync se reanuda en la próxima instrucción.

Puedes agregar una corrutina en otras corrutinas. O puedes llamar a get para bloquear 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 puedes ver, en el ejemplo de código anterior, seguiremos usando la llamada de función de bloqueo get 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.

Devolución de forma asincrónica de 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 vas a devolver asincrónicamente un tipo de Windows Runtime, debes devolver IAsyncOperation<TResult> o IAsyncOperationWithProgress<TResult, TProgress>. Se cualifica cualquier clase de tiempo de ejecución propia o de terceros o cualquier tipo que se pueda pasar a una función de Windows Runtime o 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, para que se cualifique como un corrutina, debe tener al menos una instrucción co_return o co_yield. Habrá casos en los que la corrutina pueda devolver un valor sin introducir ninguna asincronía y, por lo 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 vas a devolver de forma asincrónica un tipo que no sea de Windows Runtime, debes devolver una biblioteca de patrones de procesamiento paralelo (PPL) concurrency::task. Recomendamos concurrency::task porque ofrece un mejor rendimiento (y una mejor compatibilidad de cara al futuro) que std::future.

Tip

Si incluyes <pplawait.h>, puedes usar concurrency::task como un 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. Eso evitará la sobrecarga de copias (que implican recuento de referencias, lo que significa incrementos y decrementos entrelazados).

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

Pero puedes experimentar problemas si pasas 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 la llamada sale del ámbito. Cuando se reanuda la corrutina, puede haber ocurrido cualquier cosa en el valor de origen que hace referencia a un parámetro de referencia. Desde la perspectiva de la corrutina, un parámetro de referencia tiene un ciclo de vida incontrolado. 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 el autor de llamada destruya value, intenta acceder a él dentro de la corrutina después de que eso provoque un daño 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 de usar tras suspenderse y reanudarse, las corrutinas deben usar paso-por-valor de manera predeterminada para garantizar que capturen por valor y eviten los problemas de ciclo 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 resulte económico de mover o copiar; y normalmente es 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 tendrá ningún efecto en el valor de origen desde el que estás haciendo una copia, pero hace que el propósito sea claro y sirve de ayuda si modificas accidentalmente la copia.

// 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 puedes cambiar la firma de la corrutina, pero puedes cambiar la implementación, podrás realizar una copia local antes de la primera instrucción 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 de forma segura al puntero this en una corrutina de miembro de clase

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

API importantes