Simultaneidad avanzada y asincronía con C++/WinRT

En este tema se describen escenarios avanzados con simultaneidad y asincronía en C++/WinRT.

Para obtener una introducción a este tema, lea primero Operaciones asincrónicas y simultaneidad.

Trabajo de descarga en el grupo de subprocesos de Windows

Una corrutina es una función como cualquier otra en la que se bloquea al autor de una llamada hasta que una función le devuelva la ejecución. Y la primera oportunidad para que se devuelva una corrutina es el primer operador co_await, co_return o co_yield.

Antes de hacer un trabajo unido al cálculo en una corrutina, deberás devolver la ejecución al autor de la llamada para que no se bloquee (en otras palabras, introducir un punto de suspensión). Si aún no lo estás haciendo mediante la aplicación de co_await en alguna otra operación, puedes aplicar co_await en la función winrt::resume_background. Esto devuelve el control al autor de la llamada y reanuda inmediatamente la ejecución en un subproceso del grupo de subprocesos.

El grupo de subprocesos que se usa en la implementación es el grupo de subprocesos de bajo nivel Windows, por lo que es óptimo.

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

Programación teniendo en cuenta la afinidad de hilos

Este escenario se expande en el anterior. Descargas determinado trabajo en el grupo de subprocesos, pero quieres mostrar el progreso en la interfaz de usuario (UI).

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

El código anterior produce una excepción winrt::hresult_wrong_thread , porque un TextBlock debe actualizarse desde el subproceso que lo creó, que es el subproceso de la interfaz de usuario. Una solución es capturar el contexto del subproceso dentro del cual se llamó originalmente a nuestra corrutina. Para ello, creas una instancia de un objeto winrt::apartment_context, realizas el trabajo en segundo plano y después aplicas co_await a apartment_context para volver al contexto de llamada.

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

Mientras se llame a la corrutina anterior desde el subproceso de interfaz de usuario que creó TextBlock, esta técnica funciona. Habrá muchos casos en la aplicación donde estás seguro de eso.

Para una solución más general de actualización de la interfaz de usuario, que incluye los casos en los que no estés seguro del subproceso que realiza la llamada, puedes aplicar co_await a la función winrt::resume_foreground para cambiar a un determinado subproceso en primer plano. En el ejemplo de código siguiente, especificamos el hilo en primer plano pasando la cola de distribución asociada al TextBlock (accediendo a su propiedad DispatcherQueue). La implementación de winrt::resume_foreground llama a DispatcherQueue.TryEnqueue en ese objeto de cola de distribuidor para ejecutar el trabajo que viene después de él en la corrutina.

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

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

La función winrt::resume_foreground toma un parámetro de prioridad opcional. Si usa ese parámetro, el patrón mostrado anteriormente es adecuado. Si no es así, puede optar por simplificar co_await winrt::resume_foreground(someDispatcherObject); a solo co_await someDispatcherObject;.

Contextos de ejecución, reanudación y conmutación en una corrutina

En términos generales, después de un punto de suspensión en una corrutina, el subproceso de ejecución original puede desaparecer y se puede producir la reanudación en cualquier subproceso (en otras palabras, cualquier subproceso puede llamar al método Completed para la operación asincrónica).

No obstante, si aplicas co_await a cualquiera de los cuatro tipos de la operación asincrónica de Windows Runtime (IAsyncXxx), C++/WinRT captura el contexto de llamada en el punto en el que has aplicado co_await. Y garantiza que todavía estarás en ese contexto cuando se reanude la continuación. C++/WinRT lo hace comprobando si ya está en el contexto desde el que se realiza la llamada y, si no es así, cambiando a ese contexto. Si estabas en un subproceso de contenedor uniproceso (STA) antes de co_await, estarás en el mismo subproceso posteriormente; si estabas en un subproceso de contenedor multiproceso (MTA) antes de co_await, estarás en otro igual después.

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

La razón por la que puedes confiar en este comportamiento es porque C++/WinRT proporciona código para adaptar esos tipos de operación asincrónicas de Windows Runtime a la compatibilidad del lenguaje corrutina de C++ (estos fragmentos de código se denominan adaptadores de espera). El resto de tipos que admite await en C++/WinRT simplemente son contenedores del grupo de subprocesos o asistentes, por lo que se completan en el grupo de subprocesos.

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

Si aplica co_await a algún otro tipo (incluso dentro una implementación de corrutina de C++/WinRT), otra biblioteca proporcionará los adaptadores, y tendrá que entender lo que hacen esos adaptadores en términos de reanudación y contextos.

Para reducir al mínimo los cambios de contexto, puede usar algunas de las técnicas que ya hemos visto en este tema. Veamos algunas ilustraciones de cómo hacerlo. En este siguiente ejemplo de pseudocódigo, se muestra el esquema de un controlador de eventos que llama a una API de Windows Runtime para cargar una imagen, se coloca en un subproceso de fondo para procesar esa imagen y, a continuación, se devuelve al subproceso de interfaz de usuario para mostrar la imagen en la interfaz de usuario.

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->DispatcherQueue());

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

    // Display the image in the UI.
}

En este escenario, hay un poco de ineficacia en torno a la llamada a StorageFile::OpenAsync. Hay un cambio de contexto necesario a un subproceso en segundo plano (para que el controlador pueda devolver la ejecución al autor de la llamada), tras lo cual C++/WinRT restaura el contexto del subproceso de la interfaz de usuario. Pero, en este caso, no es necesario estar en el subproceso de interfaz de usuario hasta que vayamos a actualizar la interfaz de usuario. Cuantas más API de Windows Runtime llamemos antes de llamar a winrt::resume_background, más cambios de contexto innecesarios de ida y vuelta provocaremos. La solución es no llamar a ninguna API de Windows Runtime hasta entonces. Muévelas todas después de 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->DispatcherQueue());

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

    // Display the image in the UI.
}

Si deseas hacer algo más avanzado,puedes escribir tus propios adaptadores await. Por ejemplo, si desea que un co_await se reanude en el mismo subproceso en el que se completa la acción asíncrona (es decir, no hay ningún cambio de contexto), puede empezar escribiendo adaptadores de await similares a los que se muestran a continuación.

Note

El ejemplo de código siguiente solo se proporciona con fines educativos; es empezar a comprender cómo funcionan los adaptadores await. Si desea usar esta técnica en su propio código base, le recomendamos que desarrolle y pruebe sus propias estructuras del adaptador await. Por ejemplo, podría escribir complete_on_any, complete_on_current y complete_on(dispatcher). Considere también la posibilidad de convertirlas en plantillas que toman el tipo IAsyncXxx como parámetro de plantilla.

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

Para comprender cómo usar los adaptadores await no_switch, primero tendrá que saber que, cuando el compilador de C++ encuentra una expresión co_await, busca funciones llamadas await_ready, await_suspend y await_resume. La biblioteca de C++/WinRT proporciona esas funciones para que obtengas un comportamiento razonable de forma predeterminada, como este.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Para usar los adaptadores await no_switch, cambia el tipo de esa expresión co_await de IAsyncXxx a no_switch, del modo siguiente.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

A continuación, en lugar de buscar las tres funciones de await_xxx que coinciden con IAsyncXxx, el compilador de C++ busca funciones que coincidan con no_switch.

Análisis en profundidad de winrt::resume_foreground

A partir de C++/WinRT 2.0, la función winrt::resume_foreground suspende incluso si se llama desde el subproceso del distribuidor (en versiones anteriores, podría introducir interbloqueos en algunos escenarios porque solo se suspendió si aún no está en el subproceso del distribuidor).

El comportamiento actual implica que puedes confiar en que se produzca el desenredo de la pila y se vuelva a poner en cola. Esto es importante para la estabilidad del sistema, especialmente en el código de sistemas de bajo nivel. En la última lista de código de la sección anterior Programación teniendo en cuenta la afinidad de subprocesos se muestra cómo realizar un cálculo complejo en un subproceso en segundo plano y, después, cambiar al subproceso de interfaz de usuario adecuado para actualizar la interfaz de usuario (UI).

Este es el aspecto interno de 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{ ... };
};

Este comportamiento actual, frente al anterior, es análogo a la diferencia entre PostMessage y SendMessage en el desarrollo de aplicaciones Win32. PostMessage pone en cola el trabajo y luego desenreda la pila sin esperar a que el trabajo se complete. El desenredo de la pila puede ser esencial.

La función winrt::resume_foreground admitía originalmente CoreDispatcher (vinculada a coreWindow), que se introdujo antes de Windows 10. En las aplicaciones WinUI 3 y SDK de Aplicaciones para Windows, use la DispatcherQueue en su lugar. Puede crear un DispatcherQueue para sus propios fines. Considere esta sencilla aplicación de consola.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

En el ejemplo anterior se crea una cola (contenida en un controlador) en un subproceso privado y después se pasa el controlador a la corrutina. La corrutina puede usar la cola para esperar (suspender y reanudar) en el subproceso privado. Otro uso común de DispatcherQueue es crear una cola en el subproceso de interfaz de usuario actual para una aplicación de escritorio tradicional o 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 };
}

Esto muestra cómo puede llamar e incorporar funciones win32 en los proyectos de C++/WinRT, simplemente llamando a la función CreateDispatcherQueueController de estilo Win32 para crear el controlador y, a continuación, transferir la propiedad del controlador de cola resultante al autor de la llamada como un objeto WinRT. Así es también exactamente como puedes implementar una gestión de colas eficiente y fluida en tu aplicación de escritorio Win32 existente de estilo Petzold.

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

Anteriormente, la función principal simple comienza creando una ventana. Puede imaginar que esto registra una clase de ventana y llama a CreateWindow para crear la ventana de escritorio de nivel superior. Después, se llama a la función CreateDispatcherQueueController para crear el controlador de cola antes de llamar a alguna de las corrutinas con la cola del distribuidor que posee este controlador. Luego se introduce un suministro de mensajes tradicional en el que se produce la reanudación de la corrutina de forma natural en este subproceso. Después de hacerlo, puede volver al elegante mundo de corrutinas para el flujo de trabajo asincrónico o basado en mensajes dentro de la aplicación.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

La llamada a winrt::resume_foreground siempre pondrá en cola y después desenredará la pila. También puede establecer opcionalmente la prioridad de reanudación.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

O bien, usar el orden de cola predeterminado.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Note

Tal como se mostró anteriormente, asegúrese de incluir el encabezado de la proyección para el espacio de nombres del tipo que está co_await. Por ejemplo, Windows::System::DispatcherQueue o Microsoft::UI::Dispatching::DispatcherQueue.

O bien, en este caso, detectar el cierre de la cola y controlarlo correctamente.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

La expresión co_await devuelve true, lo que indica que la reanudación se producirá en el subproceso del distribuidor. En otras palabras, se ha puesto en cola correctamente. Por el contrario, devuelve false para indicar que la ejecución permanece en el subproceso que realiza la llamada porque el controlador de la cola se está cerrando y ya no atiende solicitudes de cola.

Por lo tanto, tienes mucho poder a tu alcance cuando combinas C++/WinRT con corrutinas; especialmente al hacer desarrollo de aplicaciones de escritorio al estilo clásico de Petzold.

Cancelación de una operación asincrónica y devoluciones de llamadas de cancelación

Las características del Windows Runtime para la programación asincrónica permiten cancelar una acción o operación asincrónica en curso. Este es un ejemplo que llama a StorageFolder::GetFilesAsync para recuperar una colección potencialmente grande de archivos y almacena el objeto de operación asincrónico resultante en un miembro de datos. El usuario tiene la opción de cancelar la operación.

// 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 Microsoft::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;
};
...

Para el lado de implementación de la cancelación, comencemos con un ejemplo sencillo.

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

Si ejecuta el ejemplo anterior, verá ImplicitCancelationAsync imprimir un mensaje por segundo durante tres segundos, después de lo cual finaliza automáticamente como resultado de la cancelación. Esto funciona porque, al encontrar una expresión co_await, una corrutina comprueba si se ha cancelado. Si lo ha hecho, se genera un cortocircuito; y si no, se suspende de la forma habitual.

La cancelación, por supuesto, se puede producir mientras la corrutina está suspendida. Solo cuando se reanuda la corrutina, o llega a otro co_await, comprobará la cancelación. El problema es una latencia potencialmente demasiado general en respuesta a la cancelación.

Por lo tanto, otra opción es sondear explícitamente la cancelación dentro de la corrutina. Actualice el ejemplo anterior con el código de la lista siguiente. En este nuevo ejemplo, ExplicitCancelationAsync recupera el objeto devuelto por la función winrt::get_cancellation_token y lo usa para comprobar periódicamente si se canceló la corrutina. Mientras no se cancele, la corrutina se ejecuta en bucle indefinidamente; cuando se cancela, el bucle y la función finalizan con normalidad. El resultado es el mismo que el ejemplo anterior, pero esta salida se produce explícitamente y bajo control.

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

Al esperar en winrt::get_cancellation_token, se recupera un token de cancelación con conocimiento de la interfaz IAsyncAction que la corrutina está produciendo en tu nombre. Puede usar el operador de llamada de la función en ese token para consultar el estado de una cancelación, básicamente, sondear en busca de una cancelación. Si vas a realizar una operación unida al cálculo o recorrer en iteración una colección de gran tamaño, entonces esta es una técnica razonable.

Registro de una devolución de llamada de cancelación

La cancelación de Windows Runtime no se propaga automáticamente a otros objetos asíncronos. Sin embargo, desde que se introdujo en la versión 10.0.17763.0 (Windows 10, versión 1809) del SDK de Windows, puede registrar una devolución de llamada de cancelación. Se trata de un enlace preventivo por el que se puede propagar la cancelación y se hace posible la integración con bibliotecas de simultaneidad existentes.

En este siguiente ejemplo de código, NestedCoroutineAsync realiza el trabajo, pero no tiene ninguna lógica de cancelación especial en él. CancelationPropagatorAsync es esencialmente un contenedor de la corrutina anidada; el contenedor reenvía una cancelación de manera preventiva.

// 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 registra una función lambda para su propia devolución de llamada de cancelación y, después, espera (se suspende) hasta que finalice el trabajo anidado. Cuando se cancela o si está cancelado CancellationPropagatorAsync, la cancelación se propaga a la corrutina anidada. No hay ninguna necesidad de sondear en busca de una cancelación, ni tampoco la cancelación se bloquea de forma indefinida. Este mecanismo es lo suficientemente flexible como para que pueda utilizarlo para interoperar con una biblioteca de corrutinas o de concurrencia que no sabe nada sobre C++/WinRT.

Informar del progreso

Si la corrutina devuelve IAsyncActionWithProgress o IAsyncOperationWithProgress, puede recuperar el objeto devuelto por la función winrt::get_progress_token y usarlo para notificar el progreso a un controlador de progreso. Este es un ejemplo de código.

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

Para informar del progreso, invoque el token de progreso con el valor de progreso como argumento. Para establecer un resultado provisional, use el set_result() método en el token de progreso.

Note

La generación de informes de resultados provisionales requiere C++/WinRT versión 2.0.210309.3 o posterior.

En el ejemplo anterior se elige establecer un resultado provisional para cada informe de progreso. Puede optar por notificar los resultados provisionales en cualquier momento, si es así. No es necesario acoplarlo con un informe de progreso.

Note

No es correcto implementar más de un controlador de finalización para una acción o operación asincrónica. Puedes tener un solo delegado para su evento completado o bien puedes aplicar co_await. Si tiene ambos, el segundo fallará. Cualquiera de los dos tipos siguientes de controladores de finalización es adecuado, pero no ambos para el mismo objeto asíncrono.

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

Para obtener más información sobre los controladores de finalización, consulte Tipos de delegado para acciones y operaciones asincrónicas.

Desencadenamiento y olvido

A veces, hay una tarea que puede realizarse en paralelo con otro trabajo, y no es necesario esperar a que se complete (ningún otro trabajo depende de esa tarea), ni que devuelva un valor. En ese caso, puedes lanzar la tarea y olvidarte de ella. Puede hacerlo escribiendo una corrutina cuyo tipo de retorno sea winrt::fire_and_forget (en lugar de alguno de los tipos de operación asíncrona de Windows Runtime o 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 también es útil como tipo de retorno del controlador de eventos cuando necesite realizar operaciones asíncronas en él. Este es un ejemplo (consulte también Referencias fuertes y débiles en 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.
}

El primer argumento (el remitente) se deja sin nombre, porque nunca lo usamos. Por ese motivo, podemos dejarlo como referencia con seguridad. Pero observa que args se pasa por valor. Consulte la sección Paso de parámetros anterior.

Esperar un controlador de kernel

C++/WinRT proporciona una función winrt::resume_on_signal , que se puede usar para suspender hasta que se señale un evento de kernel. Es tu responsabilidad asegurar que el controlador siga siendo válido hasta que se devuelva tu co_await resume_on_signal(h). resume_on_signal no puede hacerlo automáticamente, ya que es posible que haya perdido el identificador incluso antes de que se inicie el resume_on_signal , como en este primer ejemplo.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

El evento HANDLE de entrada solo es válido hasta que vuelve la función, y esta función (que es una corrutina) vuelve en el primer punto de suspensión (el primer co_await en este caso). Mientras esperas por DoWorkAsync, el control ha vuelto al autor de la llamada, el marco de la llamada ha salido del ámbito y ya no sabes si el controlador será válido cuando se reanude la corrutina.

Técnicamente, la corrutina recibe los parámetros por valor, como debería (consulta Paso de parámetros anteriormente). Pero en este caso necesitamos ir un paso más allá para que sigamos el espíritu de esa guía (en lugar de simplemente la carta). Debemos pasar una referencia segura (en otras palabras, la propiedad) junto con el controlador. Así es como.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

Pasar winrt::handle por valor proporciona la semántica de propiedad, lo que garantiza que el controlador de kernel siga siendo válido durante la vigencia de la corrutina.

Así es como podrías llamar a esa corrutina.

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

Puede pasar un valor de tiempo de espera a resume_on_signal, como en este ejemplo.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

Simplificación de los tiempos de expiración asincrónicos

C++/WinRT depende en gran medida de las corrutinas de C++. Su efecto a la hora de escribir código concurrente es revolucionario. En esta sección se describen los casos en los que los detalles de la asincronía no son importantes y, a continuación, lo único que desea es el resultado. Por ese motivo, la implementación de C++/WinRT de la interfaz de operación asincrónica de Windows Runtime IAsyncAction tiene una función get, similar a la proporcionada por std::future.

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

La función get se bloquea indefinidamente, mientras se completa el objeto asincrónico. Los objetos asíncronos suelen tener una vida útil muy corta, así que a menudo esto es todo lo que se necesita.

Pero hay casos en los que eso no es suficiente y debe abandonar la espera después de que haya transcurrido algún tiempo. Escribir ese código siempre ha sido posible, gracias a los bloques de creación proporcionados por el Windows Runtime. Pero ahora C++/WinRT facilita mucho al proporcionar la función wait_for . También se implementa en IAsyncAction y, de nuevo, es similar a la proporcionada por std::future.

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

Note

wait_for usa std::chrono::d uration en la interfaz, pero se limita a un intervalo inferior al que proporciona std::chrono::d uration (aproximadamente 49,7 días).

El wait_for en este ejemplo siguiente espera unos cinco segundos y, a continuación, comprueba la finalización. Si la comparación es favorable, sabrá que el objeto asíncrono se completó correctamente y habrá terminado. Si espera algún resultado, simplemente puede seguirlo con una llamada al método GetResults para recuperar el resultado.

Note

wait_for y get son mutuamente excluyentes (no se puede llamar a ambos). Cada una cuenta como un esperador, y las acciones u operaciones asincrónicas de Windows Runtime solo admiten un único esperador.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

Dado que el objeto asincrónico se ha completado por entonces, el método GetResults devuelve el resultado inmediatamente, sin esperar más. Como puede ver, wait_for devuelve el estado del objeto asincrónico. Por lo tanto, puede usarlo para un control más específico, como este.

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;
}
  • Recuerde que AsyncStatus::Completed significa que el objeto asincrónico se completó correctamente y puede llamar al método GetResults para recuperar cualquier resultado.
  • AsyncStatus::Canceled significa que se canceló el objeto asincrónico. Normalmente, el autor de la llamada solicita una cancelación, por lo que sería poco frecuente controlar este estado. Normalmente, un objeto asincrónico cancelado simplemente se descarta. Puede llamar al método GetResults para volver a iniciar la excepción de cancelación si lo desea.
  • AsyncStatus::Error significa que el objeto asincrónico ha fallado de alguna manera. Puede llamar al método GetResults para volver a iniciar la excepción si lo desea.
  • AsyncStatus::Started significa que el objeto asincrónico todavía se está ejecutando. El patrón asincrónico de Windows Runtime no permite múltiples esperas ni varios procesos en espera. Esto significa que no se puede llamar wait_for en un bucle. Si la espera ha agotado realmente el tiempo de expiración, te quedan algunas opciones. Puede abandonar el objeto o puede sondear su estado antes de llamar al método GetResults para recuperar cualquier resultado. Pero es mejor descartar el objeto en este momento.

Un patrón alternativo consiste en comprobar únicamente si es Started y dejar que GetResults se encargue del resto de casos.

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

Devolver una matriz de forma asincrónica

A continuación, se muestra un ejemplo de MIDL 3.0 que genera error MIDL2025: [msg]syntax error [context]: expecting > or, near "[".

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

El motivo es que no es válido usar una matriz como argumento de tipo de parámetro en una interfaz parametrizada. Por lo tanto, necesitamos una forma menos obvia de conseguir que un método de una clase del entorno de ejecución devuelva un array de forma asíncrona.

Puede devolver la matriz encapsulada en un objeto PropertyValue. A continuación, el código de llamada lo desempaqueta. Este es un ejemplo de código, que se puede probar agregando la clase en tiempo de ejecución SampleComponent a un proyecto de componente de Windows Runtime (C++/WinRT) y, a continuación, consumiéndolo desde (por ejemplo) un proyecto de aplicación en blanco, empaquetada, (WinUI 3 en escritorio).

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