Concorrenza e asincronia avanzate con C++/WinRT

Questo argomento descrive scenari più avanzati di concorrenza e asincronia in C++/WinRT.

Per un'introduzione a questo argomento, leggi prima Concorrenza e operazioni asincrone.

Operazioni di offload nel pool di thread di Windows

Una coroutine è una funzione come qualsiasi altra in cui un chiamante viene bloccato fino a quando una funzione restituisce l'esecuzione al chiamante. La prima opportunità che ha la coroutine di restituire il controllo è la prima occorrenza di co_await, co_return o co_yield.

Quindi, prima di eseguire le operazioni di associazione del calcolo (compute-bound) in una coroutine, è necessario restituire l'esecuzione al chiamante affinché non venga bloccato, inserendo in altre parole un punto di sospensione. Se ciò non è già in corso tramite l'uso di co_await per un'altra operazione, è possibile eseguire co_await per la funzione winrt::resume_background. Viene così restituito il controllo al chiamante e quindi riprende immediatamente l'esecuzione in un thread del relativo pool.

Il pool di thread usato nell'implementazione è il pool di thread Windows di basso livello, per un'efficienza ottimale.

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

Programmazione basata sull'affinità del thread

Questo scenario si basa su quello precedente. Si esegue l'offload di alcune operazioni nel pool di thread, ma poi si vuole visualizzare l'avanzamento nell'interfaccia utente.

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

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

Il codice precedente genera un'eccezione winrt::hresult_wrong_thread, perché un elemento TextBlock deve essere aggiornato dal thread che lo ha creato, ovvero il thread dell'interfaccia utente. Una soluzione consiste nell'acquisire il contesto del thread in cui è stata originariamente chiamata la coroutine. A tale scopo, crea un'istanza di un oggetto winrt::apartment_context, esegui le operazioni in background e quindi usa co_await per apartment_context per tornare al contesto chiamante.

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

Se la coroutine precedente è chiamata dal thread dell'interfaccia utente che ha creato TextBlock, questa tecnica funziona. Di ciò potrai avere la certezza in numerosi casi relativi alla tua app.

Per una soluzione più generica per l'aggiornamento dell'interfaccia utente, adatta ai casi in cui non sei certo del thread chiamante, puoi usare co_await per la funzione winrt::resume_foreground per passare a uno specifico thread in primo piano. Nell'esempio di codice riportato di seguito, specifichiamo il thread in primo piano passando l'oggetto dispatcher associato a TextBlock (accedendo alla relativa proprietà Dispatcher ). L'implementazione di winrt::resume_foreground chiama CoreDispatcher.RunAsync su tale oggetto dispatcher per eseguire le operazioni successive nella coroutine.

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

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

La funzione winrt::resume_foreground accetta un parametro di priorità facoltativo. Se si usa questo parametro, il modello illustrato in precedenza è appropriato. In caso contrario, è possibile scegliere di semplificare co_await winrt::resume_foreground(someDispatcherObject); con co_await someDispatcherObject;.

Contesti di esecuzione, ripresa e cambio di contesto in una coroutine

In termini generali, dopo un punto di sospensione in una coroutine, il thread di esecuzione originale potrebbe essere rilasciato e la ripresa potrebbe avvenire su qualsiasi thread. In altre parole, qualsiasi thread può chiamare il metodo Completed per l'operazione asincrona.

Se usi co_await per uno qualsiasi dei quattro tipi di operazione asincrona di Windows Runtime (IAsyncXxx), tuttavia, C++/WinRT acquisisce il contesto di chiamata in corrispondenza di co_await e si assicura che tale contesto sia ancora attivo al momento della ripresa. A tale scopo, C++/WinRT controlla che tu sia già nel contesto di chiamata e se non è così passa a tale contesto. Se è attivo un thread in modalità apartment a thread singolo prima di co_await, sarai nello stesso in seguito. In modo analogo, se il thread prima di co_await è multithreading, verrà usato lo stesso anche in un secondo momento.

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

Il motivo per cui puoi contare su questo comportamento è che C++/WinRT fornisce il codice per adattare questi tipi di operazione asincrona di Windows Runtime per il supporto del linguaggio per le coroutine C++ (questi frammenti di codice sono noti come adapter di attesa). I tipi awaitable rimanenti in C++/WinRT sono semplicemente wrapper di pool di thread e/o helper. Il completamento avviene pertanto nel pool di thread.

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

Se si utilizza co_await un altro tipo, anche all'interno di un'implementazione di coroutine in C++/WinRT, allora un'altra libreria fornisce gli adattatori e occorre capire cosa fanno questi adattatori in termini di ripresa e contesti.

Per ridurre al minimo i cambi di contesto, puoi usare alcune delle tecniche già viste in questo argomento. Di seguito sono disponibili alcuni esempi. Nel prossimo esempio di pseudocodice viene illustrata la struttura di un gestore eventi che chiama un'API di Windows Runtime per caricare un'immagine, passa a un thread in background per l'elaborazione di tale immagine e quindi torna al thread dell'interfaccia utente per visualizzare l'immagine nell'interfaccia utente.

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

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

    // Display the image in the UI.
}

In questo scenario, esiste un po' di inefficienza a livello della chiamata di StorageFile::OpenAsync. Esiste un cambio di contesto necessario a un thread in background (in modo che il gestore possa restituire l'esecuzione al chiamante) alla ripresa, dopo il quale C++/WinRT ripristina il contesto del thread dell'interfaccia utente. In questo caso, però, non è necessario che sia attivo il thread dell'interfaccia utente fino a quando non si sta per aggiornare l'interfaccia utente. Maggiore è il numero di API di Windows Runtime chiamate prima della chiamata a winrt::resume_background, maggiori saranno i cambi di contesto superflui da eseguire. La soluzione consiste nel non chiamare alcuna API di Windows Runtime prima della ripresa. Sposta tutte le chiamate dopo 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->Dispatcher());

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

    // Display the image in the UI.
}

Per eseguire operazioni più avanzate, puoi scrivere adapter await personalizzati. Ad esempio, se vuoi che un co_await riprenda sullo stesso thread su cui viene completata l'azione asincrona (evitando quindi il cambio di contesto), potresti iniziare a scrivere adapter await simili a quelli illustrati di seguito.

Nota

L'esempio di codice riportato di seguito viene fornito solo a scopo didattico, in modo da poter iniziare a capire come funzionano gli adapter await. Se vuoi usare questa tecnica nella tua codebase, ti consigliamo di sviluppare e testare struct per adapter await personalizzati. Potresti scrivere ad esempio complete_on_any, complete_on_current e complete_on(dispatcher). Prendi anche in considerazione la possibilità di impostarli come modelli che accettano il tipo IAsyncXxx come parametro modello.

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

Per capire come usare gli adapter await no_switch, dovrai prima di tutto tenere presente che quando il compilatore C++ rileva un'espressione co_await cerca funzioni chiamate await_ready, await_suspend e await_resume. Queste funzioni sono incluse nella libreria C++/WinRT, quindi puoi ottenere un comportamento ragionevole per impostazione predefinita, come indicato di seguito.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Per usare gli adaper await no_switch, cambia semplicemente il tipo dell'espressione co_await da IAsyncXxx a no_switch, come segue.

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

Quindi, invece di cercare tre funzioni await_xxx corrispondenti a IAsyncXxx, il compilatore C++ cerca funzioni corrispondenti a no_switch.

Informazioni dettagliate sulla funzione winrt::resume_foreground

A partire da C++/WinRT 2.0, la funzione winrt::resume_foreground viene sospesa anche se viene chiamata dal thread del dispatcher. Nelle versioni precedenti può introdurre deadlock in alcuni scenari perché viene sospesa solo se non è già presente nel thread del dispatcher.

Il comportamento corrente significa che puoi fare affidamento sulla rimozione dello stack e sulla ripetizione dell'accodamento. Questo aspetto è importante per la stabilità del sistema, soprattutto nel codice di sistemi di ultimo livello. L'ultimo listato di codice nella sezione Programmazione basata sull'affinità del thread sopra riportata illustra l'esecuzione di calcoli complessi in un thread in background e quindi il passaggio al thread dell'interfaccia utente appropriato per aggiornare l'interfaccia utente.

Viene riportato di seguito l'aspetto interno di 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{ ... };
};

Questo comportamento corrente, confrontato con quello precedente, è analogo alla differenza tra PostMessage e SendMessage nello sviluppo di applicazioni Win32. PostMessage accoda il lavoro e quindi rimuove lo stack senza attendere il completamento del lavoro. La rimozione dello stack può essere essenziale.

Anche la funzione winrt::resume_foreground supporta inizialmente solo la classe CoreDispatcher (associata a CoreWindow), introdotta prima di Windows 10. Da allora è stato introdotto un dispatcher più flessibile ed efficiente, ovvero DispatcherQueue. Puoi creare un oggetto DispatcherQueue per i tuoi scopi. Esamina questa semplice applicazione console.

using namespace Windows::System;

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

L'esempio precedente crea una coda (contenuta in un controller) in un thread privato e quindi passa il controller alla coroutine. La coroutine può usare la coda per l'attesa (sospensione e ripresa) sul thread privato. Un altro uso comune di DispatcherQueue consiste nel creare una coda sul thread dell'interfaccia utente corrente per un'app desktop o Win32 tradizionale.

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

Questo esempio illustra come puoi chiamare e incorporare le funzioni Win32 nei progetti C++/WinRT chiamando semplicemente la funzione CreateDispatcherQueueController di tipo Win32 per creare il controller e quindi trasferire al chiamante la proprietà del controller della coda risultante come oggetto WinRT. Questo è anche esattamente il modo in cui puoi supportare un accodamento efficiente e senza problemi in un'applicazione desktop Win32 di tipo Petzold esistente.

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

Nell'esempio sopra riportato la funzione main semplice inizia creando una finestra. Come puoi immaginare, questa operazione registra una classe window e chiama CreateWindow per creare la finestra desktop di primo livello. Viene quindi chiamata la funzione CreateDispatcherQueueController per creare il controller della coda prima di chiamare una coroutine con la coda del dispatcher di proprietà del controller. Viene quindi immesso un message pump tradizionale in cui la ripresa della coroutine si verifica naturalmente in questo thread. A questo punto puoi tornare al mondo elegante delle coroutine per il flusso di lavoro asincrono o basato su messaggi nell'applicazione.

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

La chiamata a winrt::resume_foreground eseguirà sempre l'accodamento (queue) e quindi rimuoverà lo stack. Puoi anche facoltativamente impostare la priorità di ripresa.

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

In alternativa, puoi usare l'ordine di esecuzione delle query predefinito.

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

Nota

Come mostrato sopra, assicurarsi di includere l'intestazione della proiezione per lo spazio dei nomi del tipo che si sta co_await-ing. Ad esempio, Windows::UI::Core::CoreDispatcher, Windows::System::D ispatcherQueueo Microsoft::UI::D ispatching::D ispatcherQueue.

In alternativa, in questo caso, puoi rilevare l'arresto della coda e gestirlo normalmente.

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

L'espressione co_await restituisce true, indicando che la ripresa si verificherà sul thread del dispatcher. In altre parole, l'accodamento ha avuto esito positivo. Al contrario, restituisce false per indicare che l'esecuzione rimane sul thread chiamante perché il controller della coda sta per essere arrestato e non è più in grado di soddisfare le richieste di accodamento.

Hai quindi molte possibilità quando combini C++/WinRT con le coroutine, soprattutto nel caso di sviluppo di un'applicazione desktop di tipo Petzold tradizionale.

Annullamento di un'operazione asincrona e callback di annullamento

Le funzionalità di Windows Runtime per la programmazione asincrona consentono di annullare un'azione o un'operazione asincrona in corso. Di seguito è riportato un esempio che chiama StorageFolder::GetFilesAsync per recuperare una raccolta di file potenzialmente grande e archivia l'oggetto operazione asincrona risultante in un membro dati. L'utente ha la possibilità di annullare l'operazione.

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

Per illustrare l'implementazione dell'annullamento, iniziamo con un semplice esempio.

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

Se esegui l'esempio precedente, ImplicitCancelationAsync stamperà un messaggio al secondo per tre secondi e dopo questo intervallo verrà terminato automaticamente come risultato dell'annullamento. Questa procedura funziona perché, in presenza di un'Expression co_await, una coroutine controlla se è stata annullata. In caso affermativo, viene generato un corto circuito e in caso contrario viene sospesa normalmente.

L'annullamento può naturalmente verificarsi mentre la coroutine è sospesa. Solo alla ripresa o in presenza di un altro co_await, la coroutine controllerà se è stato richiesto l'annullamento. Il problema è una latenza potenzialmente con granularità troppo grossolana per la risposta all'annullamento.

Un'altra opzione prevede quindi il polling esplicito per l'annullamento dall'interno della coroutine. Aggiorna l'esempio precedente con il codice nel listato seguente. In questo nuovo esempio, ExplicitCancelationAsync recupera l'oggetto restituito dalla funzione winrt::get_cancellation_token e lo usa per controllare periodicamente se le coroutine è stata annullata. Fino a quando non viene annullata, le coroutine esegue un ciclo infinito. Dopo l'annullamento, il ciclo e la funzione si concludono normalmente. Il risultato è lo stesso dell'esempio precedente, ma in questo caso la chiusura avviene in modo esplicito e sotto controllo.

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

L'attesa di winrt::get_cancellation_token consente di recuperare un token di annullamento con conoscenza dell'azione IAsyncAction che la coroutine genera automaticamente. È possibile usare l'operatore di chiamata di funzione su tale token per richiedere lo stato di annullamento, operazione che corrisponde fondamentalmente al polling per l'annullamento. Se esegui operazioni associate al calcolo o l'iterazione di una raccolta di grandi dimensioni, questa è una tecnica ragionevole.

Registrare un callback di annullamento

L'annullamento di Windows Runtime non si propaga automaticamente ad altri oggetti asincroni. A partire dalla versione 10.0.17763.0 (Windows 10, versione 1809) di Windows SDK è però possibile registrare un callback di annullamento. Si tratta di un hook preventivo tramite il quale può essere propagato l'annullamento e che rende possibile l'integrazione con le librerie di concorrenza esistenti.

Nel prossimo esempio di codice NestedCoroutineAsync esegue il lavoro, ma non include alcuna logica di annullamento speciale. CancelationPropagatorAsync è essenzialmente un wrapper per la coroutine annidata, che inoltra l'annullamento preventivamente.

// 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 funzione lambda per il callback di annullamento e quindi attende (viene sospeso) fino al completamento delle operazioni annidate. In caso di annullamento di CancellationPropagatorAsync, l'annullamento viene propagato alla coroutine annidata. Non è necessario eseguire il polling per l'annullamento e l'annullamento non viene bloccato per un periodo illimitato. Questo meccanismo è sufficientemente flessibile per poterlo usare per l'interoperabilità con una coroutine o una libreria di concorrenza senza alcuna conoscenza di C++/WinRT.

Segnalazione dello stato

Se la coroutine restituisce IAsyncActionWithProgress, o IAsyncOperationWithProgress, puoi recuperare l'oggetto restituito dalla funzione winrt::get_progress_token e usarlo per segnalare lo stato a un gestore dello stato. Ecco un esempio di codice.

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

Per segnalare lo stato di avanzamento, richiamare il token di stato con il valore di stato come argomento. Per impostare un risultato provvisorio, usare il metodo set_result() nel token di stato.

Nota

La creazione di report provvisori richiede C++/WinRT versione 2.0.210309.3 o successiva.

Nell'esempio precedente viene scelto di impostare un risultato provvisorio per ogni report sullo stato di avanzamento. È possibile scegliere di segnalare i risultati provvisori in qualsiasi momento, se affatto. Non è necessario associarlo a un rapporto sullo stato di avanzamento.

Nota

Non è corretto implementare più di un gestore del completamento per un'operazione o un'azione asincrona. Puoi avere un solo delegato per l'evento completato oppure puoi usare co_await. In presenza di entrambi, il secondo avrà esito negativo. Solo uno dei due tipi di gestori del completamento seguenti è appropriato. Entrambi non sono consentiti per lo stesso oggetto asincrono.

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

Per altre informazioni sui gestori di completamento, vedi Tipi di delegati per operazioni e azioni asincrone.

Attivare e poi dimenticare

In alcuni casi puoi avere un'attività che può essere eseguita contemporaneamente ad altre operazioni, ma non hai la necessità di attendere il completamento dell'attività (ovvero non ci sono altre operazioni dipendenti) e non serve neanche restituire un valore. In questo caso puoi attivare l'attività e poi dimenticarla. Puoi farlo scrivendo una coroutine con tipo restituito winrt::fire_and_forget (invece di uno dei tipi di operazione asincrona di 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 è utile anche come tipo restituito del gestore eventi quando devi eseguirvi operazioni asincrone. Ecco un esempio (vedi anche Riferimenti sicuri e deboli in 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.
}

Il primo argomento (il sender) è stato lasciato senza nome perché non viene mai usato. Per questo motivo è corretto lasciarlo come riferimento. Puoi tuttavia notare che args viene passato per valore. Vedi la sezione precedente Passaggio di parametri.

Attesa di un handle del kernel

C++/WinRT offre una funzione winrt::resume_on_signal, che è possibile usare per sospendere il processo finché non viene segnalato un evento del kernel. Sei tu ad avere la responsabilità di garantire che l'handle rimanga valido fino a quando co_await resume_on_signal(h) non restituisce il controllo. resume_on_signal non è in grado di eseguire questa operazione, perché potresti aver perso l'handle ancora prima dell'avvio di resume_on_signal, come in questo primo esempio.

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

L'HANDLE in ingresso è valido solo fino a quando la funzione non restituisce il controllo e questa funzione, che è una coroutine, restituisce il controllo in corrispondenza del primo punto di sospensione (il primo co_await, in questo caso). Durante l'attesa di DoWorkAsync, il controllo è tornato al chiamante, il frame chiamante è uscito dall'ambito e non puoi più sapere se l'handle sarà valido al riavvio della coroutine.

Tecnicamente, la coroutine riceve i parametri per valore, come necessario (vedi la sezione precedente Passaggio di parametri). In questo caso, tuttavia, dobbiamo compiere un passo un più in modo da seguire lo spirito, e non solo la lettera, di tali indicazioni. Dobbiamo passare un riferimento sicuro (in altre parole, la proprietà) insieme all'handle. Ecco come.

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

Il passaggio di winrt::handle per valore fornisce una semantica di proprietà in base alla quale hai la garanzia che l'handle del kernel rimanga valido per l'intera durata della coroutine.

Ecco come puoi chiamare tale coroutine.

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

È possibile passare un valore di timeout a resume_on_signal, come illustrato in questo esempio.

winrt::handle event = ...

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

Semplificazione dei timeout asincroni

C++/WinRT ha investito in modo significativo sulle coroutine C++. Il loro effetto sulla scrittura di codice di concorrenza fa la differenza. Questa sezione illustra i casi in cui i dettagli dell'asincronia non sono importanti e si vuole solo ottenere il risultato all'istante. Per questo motivo, l'implementazione C++/WinRT dell'interfaccia di operazioni asincrone Windows Runtime IAsyncAction ha una funzione get simile a quella fornita da std::future.

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

La funzione get effettua un blocco a tempo indeterminato mentre l'oggetto asincrono viene completato. Gli oggetti asincroni tendono a essere di breve durata, quindi questo è spesso tutto ciò di cui hai bisogno.

In alcuni casi, tuttavia, questo non è sufficiente e devi abbandonare l'attesa dopo che è trascorso un po' di tempo. La scrittura di tale codice è sempre stata possibile grazie ai blocchi predefiniti forniti da Windows Runtime. Ora però C++/WinRT rende molto più semplice questa operazione con la funzione wait_for. Questa funzione è implementata anche in IAsyncAction e, anche in questo caso, è simile a quella fornita da std::future.

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

Nota

wait_for usa std::chrono::duration nell'interfaccia, ma è limitata a un intervallo più piccolo di quanto fornito da std::chrono::duration (circa 49,7 giorni).

La funzione wait_for nell'esempio seguente attende circa cinque secondi, quindi controlla il completamento. Se il confronto ha esito positivo, sai che l'oggetto asincrono è stato completato correttamente. Se attendi un risultato, puoi semplicemente far seguire a questa procedura una chiamata alla funzione GetResults per recuperare il risultato.

Nota

wait_for e get si escludono a vicenda (non è possibile chiamarle entrambe). Ognuno di essi viene conteggiato come waitere le azioni/operazioni asincrone di Windows Runtime supportano solo un singolo waiter.

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

Poiché per allora l'oggetto asincrono risulterà completato, la funzione GetResults restituisce immediatamente il risultato, senza richiedere un'ulteriore attesa. Come puoi notare, wait_for restituisce lo stato dell'oggetto asincrono. Puoi pertanto usare questa funzione per un controllo più granulare, come indicato di seguito.

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;
}
  • Tieni presente che AsyncStatus::Completed indica che l'oggetto asincrono è stato completato correttamente e puoi chiamare la funzione GetResults per recuperare eventuali risultati.
  • AsyncStatus::Canceled indica che l'oggetto asincrono è stato annullato. Un annullamento viene in genere richiesto dal chiamante, quindi è raro gestire questo stato. In genere un oggetto asincrono annullato viene semplicemente rimosso. Se lo si desidera, è possibile chiamare il metodo GetResults per generare nuovamente l'eccezione di annullamento.
  • AsyncStatus::Error indica che in qualche modo l'oggetto asincrono ha avuto esito negativo. È possibile chiamare il metodo GetResults per rigenerare l'eccezione di annullamento, se lo si desidera.
  • AsyncStatus::Started indica che l'oggetto asincrono è ancora in esecuzione. Il modello asincrono di Windows Runtime non consente né attese multiple, né waiter. Ciò significa che non puoi chiamare wait_for in un ciclo. Se si è verificato il timeout dell'attesa, le scelte disponibili sono poche. Può abbandonare l'oggetto oppure eseguire il polling dello stato prima di chiamare get per recuperare i risultati. È tuttavia preferibile rimuovere l'oggetto a questo punto.

Un modello alternativo consiste nel verificare solo Startede consentire a GetResults di gestire gli altri casi.

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

Restituzione di una matrice in modo asincrono

Di seguito è riportato un esempio di MIDL 3.0 che genera l’errore MIDL2025: [msg]syntax error [context]: expecting >accanto a "[").

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

L'errore è dovuto al fatto che non è possibile usare una matrice come argomento di tipo parametro per un'interfaccia con parametri. È quindi necessario un modo meno ovvio per riuscire a passare in modo asincrono una matrice da un metodo della classe di runtime.

Puoi restituire la matrice sottoposta a conversione boxing in un oggetto PropertyValue. Il codice chiamante ne eseguirà quindi l'unboxing. Di seguito è riportato un esempio di codice che puoi provare a usare aggiungendo la classe di runtime SampleComponent a un progetto Windows Runtime Component (C++/WinRT) e quindi utilizzando tale classe da un altro progetto, ad esempio Core App (C++/WinRT).

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