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.

Delegación de trabajo al grupo de subprocesos de Windows

Una corrutina es una función como cualquier otra, en el sentido de que quien la invoca queda bloqueado hasta que la función le devuelve el control de ejecución. Y la primera oportunidad para que una corrutina retorne es el primer co_await, co_return o co_yield.

Por lo tanto, antes de realizar en una corrutina tareas intensivas de cálculo, debes devolver la ejecución al código que la invocó (es decir, introducir un punto de suspensión) para no bloquearlo. Si aún no está haciendo eso mediante co_await alguna otra operación, puede co_await la función winrt::resume_background. Eso devuelve el control al código que realiza la llamada y, a continuación, reanuda inmediatamente la ejecución en un hilo 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. Delega parte del trabajo en el grupo de subprocesos, pero luego quiere 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 consiste en capturar el contexto del hilo dentro del cual se llamó originalmente a nuestra corrutina. Para ello, cree una instancia de un objeto winrt::apartment_context , realice el trabajo en segundo plano y, después co_await , el 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.
}

Siempre que 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 para actualizar la interfaz de usuario, que cubre los casos en los que no está seguro de cuál es el subproceso de llamada, puede co_await usar la función winrt::resume_foreground para cambiar a un subproceso de primer plano específico. 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 sobre ese objeto DispatcherQueue para ejecutar el trabajo que le sigue 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 original de ejecución puede desaparecer y la reanudación puede producirse en cualquier subproceso (es decir, cualquier subproceso puede llamar al método Completed para la operación asincrónica).

Pero si co_await cualquiera de los cuatro tipos de operación asincrónica de Windows Runtime (IAsyncXxx), C++/WinRT captura el contexto de llamada en el momento en que co_await. Y asegura que sigues en ese mismo contexto cuando la continuación se reanuda. 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 apartamento de un solo subproceso (STA) antes de co_await, después seguirás en el mismo; si estabas en un subproceso de apartamento de varios subprocesos (MTA) antes de co_await, después seguirás en uno.

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). Los demás tipos esperables de C++/WinRT son simplemente envoltorios del grupo de subprocesos o utilidades auxiliares; de modo 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 usa co_await algún otro tipo, incluso dentro de una implementación de corrutina de C++/WinRT, otra biblioteca proporcionará los adaptadores, y tendrá que entender qué hacen esos adaptadores en cuanto a la reanudación y los 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 hilo de la interfaz de usuario hasta justo antes de actualizar la interfaz. 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 no debe llamar a ninguna API de Windows Runtime antes. Muévalos todos 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 quiere hacer algo más avanzado, puede escribir sus propios adaptadores de 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, solo hay que cambiar el tipo de esa expresión co_await de IAsyncXxx a no_switch, como esto.

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 significa que puede confiar en el desenredado de la pila y la puesta en cola; y eso es importante para la estabilidad del sistema, especialmente en el código de sistemas de bajo nivel. El último fragmento de código de la sección Programación con afinidad de subproceso en mente, más arriba, ilustra cómo realizar un cálculo complejo en un subproceso en segundo plano y, a continuación, cambiar al subproceso adecuado de la interfaz de usuario 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, a continuación, desenreda la pila sin esperar a que se complete el trabajo. El desenrollado 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 dentro de un controlador) en un hilo privado y después se pasa el controlador a la corrutina. La corrutina puede usar la cola para esperar (suspenderse y reanudarse) en el hilo 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. A continuación, se llama a la función CreateDispatcherQueueController para crear el controlador de cola antes de llamar a una corrutina con la cola de distribución que pertenece a este controlador. A continuación, se introduce una bomba de mensajes tradicional donde la reanudación de la corrutina se produce naturalmente 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 se pondrá en cola y, a continuación, 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, con el orden de puesta en cola predeterminado.

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

Note

Como se muestra arriba, asegúrese de incluir la cabecera de proyección para el espacio de nombres del tipo con el que está trabajando co_await. Por ejemplo, Windows::System::DispatcherQueue o Microsoft::UI::Dispatching::DispatcherQueue.

O, en este caso, detectando el cierre de la cola y gestionándolo adecuadamente.

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 despachador. En otras palabras, esa puesta en cola se realizó correctamente. Por el contrario, devuelve false para indicar que la ejecución permanece en el hilo que realiza la llamada porque el controlador de la cola se está apagando y ya no atiende las solicitudes a la 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 llamada 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 la expresión co_await, una corrutina comprueba si ha sido cancelada. Si lo tiene, entonces cortocircuita; y si no lo tiene, se suspende como normal.

La cancelación puede ocurrir, por supuesto, mientras la corrutina está suspendida. Solo cuando se reanude la corrutina o alcance otro co_await, verificará si se ha cancelado. El problema radica en una latencia potencialmente demasiado alta al responder a la cancelación.

Por lo tanto, otra opción es sondear explícitamente la cancelación desde 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 a winrt::get_cancellation_token, se obtiene un token de cancelación con información sobre la IAsyncAction que la corrutina está produciendo por usted. Puede usar el operador de llamada de función sobre ese token para consultar el estado de cancelación; es decir, realizando un sondeo del estado de cancelación. Si va a realizar alguna operación intensiva en cálculo o está iterando sobre una colección grande, 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. Pero, a partir de la versión 10.0.17763.0 (Windows 10, versión 1809) del SDK de Windows, puede registrar una función de devolución de llamada de cancelación. Se trata de un mecanismo de enlace preventivo que permite propagar la cancelación y posibilita la integración con las bibliotecas de concurrencia 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 envoltorio de la corrutina anidada; el envoltorio propaga la cancelación de manera anticipada.

// 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 como su propio callback de cancelación y, a continuación, queda a la espera (se suspende) hasta que finaliza la operación anidada. Cuando o si se cancela CancellationPropagatorAsync, propaga la cancelación a la corrutina anidada. No es necesario sondear la cancelación; tampoco se bloquea indefinidamente la cancelación. 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. Puede tener un único delegado para su evento completado o puede co_await hacerlo. 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.

Fuego 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, puede desencadenar la tarea y olvidarla. 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.

Esperando un identificador 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. Usted es responsable de asegurarse de que el identificador sigue siendo válido hasta co_await resume_on_signal(h) que se devuelva. 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 HANDLE de entrada solo es válido hasta que la función retorna, y esta función (que es una corrutina) retorna en el primer punto de suspensión (el primer co_await en este caso). Mientras espera DoWorkAsync, el control ha vuelto al autor de la llamada, el marco de llamada se ha quedado fuera del ámbito y ya no sabe si el identificador será válido cuando se reanude la corrutina.

Técnicamente, nuestra corrutina recibe sus parámetros por valor, como debe ser (véase paso de parámetros más arriba). 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). Es necesario pasar una referencia segura (en otras palabras, propiedad) junto con el identificador. Así es como.

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

Pasar un winrt::handle por valor proporciona semántica de propiedad, lo que garantiza que el identificador del 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");
}

Tiempos de espera asíncronos de forma sencilla

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 uno cuenta como una espera, y las acciones u operaciones asincrónicas de Windows Runtime solo admiten una única espera.

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 superado en la práctica el tiempo de espera, entonces te quedan unas pocas 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 produce error MIDL2025: [msg]error de sintaxis [context]: se esperaba > o, cerca de "[".

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. Aquí tienes un ejemplo de código que puedes probar añadiendo la clase de tiempo de ejecución SampleComponent a un proyecto Componente de Windows Runtime (C++/WinRT) y usándola después desde, por ejemplo, un proyecto Aplicación en blanco, empaquetada (WinUI 3 de 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