Concorrenza e operazioni asincrone con C++/WinRT

Importante

Questo argomento presenta i concetti di coroutine e co_await, che consigliamo di usare nelle applicazioni dell'interfaccia utente e in quelle non di interfaccia utente. Per semplicità, la maggior parte degli esempi di codice forniti in questo argomento introduttivo illustra progetti di applicazione console di Windows (C++/WinRT). Gli ultimi esempi di codice di questo argomento usano le coroutine, ma per praticità gli esempi di applicazione console continuano anche a usare la chiamata della funzione get di blocco appena prima di uscire, in modo che l'applicazione non venga chiusa prima di terminare la stampa dell'output. Non devi eseguire questa operazione (chiamata della funzione get di blocco) da un thread dell'interfaccia utente. Devi invece usare l'istruzione co_await. Le tecniche da usare nelle applicazioni dell'interfaccia utente sono descritte nell'argomento Concorrenza e asincronia avanzate.

Questo argomento introduttivo illustra alcuni dei modi per creare e utilizzare oggetti asincroni di Windows Runtime con C++/WinRT. Dopo la lettura di questo argomento, in particolare per le tecniche da usare nelle applicazioni dell'interfaccia utente, vedi anche Concorrenza e asincronia avanzate.

Operazioni asincrone e funzioni "Async" di Windows Runtime

Qualsiasi API Windows Runtime per il cui completamento possono essere richiesti più di 50 millisecondi viene implementata come funzione asincrona (con un nome che termina con "Async"). L'implementazione di una funzione asincrona avvia il lavoro su un altro thread e restituisce immediatamente un oggetto che rappresenta l'operazione asincrona. Al termine dell'operazione asincrona, l'oggetto restituito contiene qualsiasi valore risultante dal lavoro. Lo spazio dei nomi Windows Runtime Windows::Foundation contiene quattro tipi di oggetti operazione asincrona.

Ognuno di questi tipi di operazione asincrona viene proiettato in un tipo corrispondente nello spazio dei nomi C++/WinRT winrt::Windows::Foundation. C++/WinRT contiene anche uno struct adapter await interno. Non viene usato direttamente, ma grazie a tale struct, puoi scrivere un'istruzione co_await per attendere in modo cooperativo il risultato di qualsiasi funzione che restituisce uno di questi tipi di operazione asincrona. Puoi anche creare coroutine personali che restituiscono questi tipi.

Un esempio di funzione asincrona di Windows è SyndicationClient::RetrieveFeedAsync, che restituisce un oggetto operazione asincrona di tipo IAsyncOperationWithProgress<TResult, TProgress>.

Esaminiamo alcuni modi (prima con blocco e poi senza blocco) di usare C++/WinRT per chiamare un'API come questa. Solo per illustrare le idee di base, nei prossimi esempi di codice useremo un progetto di applicazione console di Windows (C++/WinRT). Le tecniche più appropriate per un'applicazione dell'interfaccia utente sono descritte in Concorrenza e asincronia avanzate.

Bloccare il thread chiamante

L'esempio di codice seguente riceve un oggetto operazione asincrona da RetrieveFeedAsync e chiama get su tale oggetto per bloccare il thread chiamante fino a quando non sono disponibili i risultati dell'operazione asincrona.

Se vuoi copiare e incollare questo esempio direttamente nel file di codice sorgente principale di un progetto Applicazione console Windows (C++/WinRT), imposta prima di tutto Senza intestazioni precompilate nelle proprietà del progetto.

// main.cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

La chiamata a get semplifica la scrittura del codice ed è ideale per app console e thread in background nei casi in cui non vuoi usare una coroutine per qualsiasi motivo. Dato che non è una chiamata asincrona né simultanea, non è appropriata per un thread dell'interfaccia utente. Se tenti di usarne una, verrà generata un'asserzione nelle build non ottimizzate. Per evitare ritardi nell'esecuzione di altre utili attività da parte dei thread del sistema operativo, è necessario usare una tecnica diversa.

Scrivere una coroutine

C++/WinRT integra le coroutine C++ nel modello di programmazione per fornire un modo naturale per attendere un risultato in modo cooperativo. Puoi creare un'operazione asincrona Windows Runtime personalizzata scrivendo una coroutine. Nell'esempio di codice seguente ProcessFeedAsync è la coroutine.

Nota

La funzione get esiste nel tipo di proiezione C++/WinRT winrt::Windows::Foundation::IAsyncAction, quindi puoi chiamare la funzione da qualsiasi progetto C++/WinRT. Non troverai la funzione elencata come membro dell'interfaccia IAsyncAction, perché get non fa parte della superficie dell'interfaccia ABI (Application Binary Interface) del tipo effettivo di Windows Runtime IAsyncAction.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    PrintFeed(syndicationFeed);
}

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

Una coroutine è una funzione che può essere sospesa e ripresa. Nella precedente coroutine ProcessFeedAsync, quando l'istruzione co_await viene raggiunta, la coroutine esegue la chiamata a RetrieveFeedAsync in modo asincrono e quindi sospende immediatamente se stessa e restituisce il controllo al chiamante (main nell'esempio precedente). main può quindi continuare a lavorare mentre il feed viene recuperato e stampato. Al termine, ovvero al completamento della chiamata a RetrieveFeedAsync, la coroutine ProcessFeedAsync riprende in corrispondenza dell'istruzione successiva.

Puoi aggregare una coroutine in altre coroutine, puoi chiamare get per applicare il blocco e attenderne il completamento, per ottenere il risultato, se disponibile, oppure puoi passare a un altro linguaggio di programmazione che supporta Windows Runtime.

È anche possibile gestire gli eventi completati e/o in corso di operazioni e azioni asincrone tramite i delegati. Per informazioni dettagliate ed esempi di codice, vedi Tipi di delegati per operazioni e azioni asincrone.

Come puoi notare, nell'esempio di codice precedente continuiamo a usare la chiamata della funzione get di blocco appena prima di uscire da main. Lo scopo è impedire che l'applicazione venga chiusa prima di terminare la stampa dell'output.

Restituire un tipo Windows Runtime in modo asincrono

Nell'esempio seguente viene eseguito il wrapping di una chiamata a RetrieveFeedAsync per un URI specifico, per ottenere una funzione RetrieveBlogFeedAsync che restituisce SyndicationFeed in modo asincrono.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

Nell'esempio precedente RetrieveBlogFeedAsync restituisce un oggetto IAsyncOperationWithProgress, che contiene entrambi i valori progress e return. È possibile eseguire altre attività mentre la funzione RetrieveBlogFeedAsync viene eseguita e recupera il feed. Chiamiamo quindi get su tale oggetto operazione asincrona per applicare il blocco, attendere il completamento e quindi ottenere i risultati dell'operazione.

Se intendi restituire in modo asincrono un tipo Windows Runtime, devi restituire un oggetto IAsyncOperation<TResult> o IAsyncOperationWithProgress<TResult, TProgress>. Risultano appropriati qualsiasi classe di runtime di prima o terza parte oppure qualsiasi tipo che può essere passato verso o da una funzione di Windows Runtime (ad esempio, int o winrt::hstring). Il compilatore sarà di aiuto con un errore che indica che "T deve essere di tipo WinRT" se si prova a usare uno di questi tipi di operazione asincrona con un tipo non Windows Runtime.

Se una coroutine non include almeno un'istruzione co_await, per essere qualificata come coroutine deve avere almeno un'istruzione co_returno co_yield. In alcuni casi la coroutine potrà restituire un valore senza introdurre un'operazione asincrona e dunque senza bloccare né cambiare contesto. Di seguito è riportato un esempio in cui ciò avviene (la seconda volta e le successive in cui viene chiamata) tramite la memorizzazione di un valore nella cache.

winrt::hstring m_cache;

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

Restituire un tipo non Windows Runtime in modo asincrono

Se intendi restituire in modo asincrono un tipo non Windows Runtime, devi restituire una classe concurrency::task PPL (Parallel Patterns Library). È consigliabile usare concurrency::task perché consente prestazioni migliori (e migliore compatibilità futura) rispetto a std::future.

Suggerimento

Se includi <pplawait.h>, potrai usare concurrency::task come tipo di coroutine.

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

Passaggio di parametri

Per le funzioni sincrone è consigliabile usare i parametri const& per impostazione predefinita. Ciò eviterà l'overhead delle copie, che implica il conteggio dei riferimenti, con conseguenti aumenti e diminuzioni interlocked.

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

Possono tuttavia verificarsi problemi se passi un parametro di riferimento a una coroutine.

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

In una coroutine l'esecuzione è sincrona fino al primo punto di sospensione, dove il controllo viene restituito al chiamante e il frame chiamante esce dall'ambito. Alla ripresa della coroutine potrebbe essere successo qualsiasi cosa al valore di origine a cui fa riferimento un parametro di riferimento. Dal punto di vista della coroutine, un parametro di riferimento ha una durata non controllata. Nell'esempio precedente possiamo accedere in sicurezza a value fino a co_await, ma non dopo. Nel caso in cui value venga distrutto dal chiamante, il tentativo di accedervi all'interno della coroutine dopo la distruzione comporta un danneggiamento della memoria. Allo stesso modo, possiamo passare in modo sicuro value a DoOtherWorkAsync qualora ci sia la possibilità che la funzione venga sospesa e quindi provare a usare value alla ripresa.

Per fare in modo che i parametri siano utilizzabili in modo sicuro dopo la sospensione e la ripresa, le coroutine devono usare pass-by-value per impostazione predefinita, al fine di garantire che eseguano l'acquisizione in base al valore ed evitare problemi di durata. Sono rari i casi in cui puoi evitare di seguire queste indicazioni con la certezza che non comporti problemi di sicurezza.

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

Affinché venga passato per valore, l'argomento non deve essere dispendioso da spostare o copiare. Questo è in genere il caso di un puntatore intelligente.

È anche discutibile che sia buona norma passare il valore const (a meno che non si voglia spostarlo). Ciò non avrà alcun effetto sul valore di origine da cui effettui una copia, ma rende chiara l'intenzione e risulta utile in caso di modifica accidentale della copia.

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

Vedi anche Vettori e matrici standard, in cui viene descritto come passare un vettore standard in un computer chiamato asincrono.

Se non riesci a modificare la firma della coroutine, ma riesci a modificare l'implementazione, puoi creare una copia locale prima della prima istanza di co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Se Param è dispendioso da copiare, estrai solo le parti necessarie prima della prima istanza di co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

Accesso sicuro al puntatore this in una coroutine membro di classe

Vedi Riferimenti sicuri e deboli in C++/WinRT.

API importanti