Programmazione asincrona in C++/CX
Nota
Questo argomento è utile per gestire l'applicazione C++/CX. Ma consigliamo di usare C++/WinRT per le nuove applicazioni. C++/WinRT è una proiezione del linguaggio C++ 17 interamente standard e moderna per le API di Windows Runtime (WinRT), implementata come libreria basata su file di intestazione e progettata per fornirti accesso privilegiato alla moderna API di Windows.
In questo articolo viene descritto il modo consigliato per utilizzare i metodi asincroni nelle estensioni dei componenti di Visual C++ (C++/CX) utilizzando la task
classe definita nello concurrency
spazio dei nomi in ppltasks.h.
Tipi asincroni di Windows Runtime
Windows Runtime offre un modello ben definito per chiamare metodi asincroni e fornisce i tipi necessari per utilizzare tali metodi. Se non si ha familiarità con il modello asincrono di Windows Runtime, leggere Programmazione asincrona prima di leggere il resto di questo articolo.
Sebbene sia possibile utilizzare le API di Windows Runtime asincrone direttamente in C++, l'approccio preferito consiste nell'usare la task class e i relativi tipi e funzioni correlati, contenuti nello spazio dei nomi concurrency e definito in <ppltasks.h>
. La concurrency::task è un tipo per utilizzo generico, ma quando l'opzione del compilatore /ZW, necessaria per le app e i componenti UWP (Universal Windows Platform), viene usata la classe di attività incapsula i tipi asincroni di Windows Runtime in modo che sia più semplice:
concatenare più operazioni asincrone e sincrone
gestire le eccezioni nelle catene di attività
eseguire l'annullamento nelle catene di attività
assicurarsi che le singole attività vengano eseguite nel contesto di thread o nell'apartment appropriato
Questo articolo fornisce indicazioni di base su come usare la classe task con le API asincrone di Windows Runtime. Per una documentazione più completa su task e sui relativi metodi, tra cui create_task, vedere Task Parallelism (Concurrency Runtime).
Utilizzo di un'operazione asincrona tramite un'attività
Nell'esempio seguente viene illustrato come utilizzare la classe task per utilizzare un metodo async e restituisce un'interfaccia IAsyncOperation e la cui operazione produce un valore. I passaggi principali sono indicati di seguito.
Chiamare il
create_task
metodo e passarlo all'oggetto IAsyncOperation^.Chiamare la funzione membro task::then nell'attività e specificare un'espressione lambda che verrà richiamata al termine dell'operazione asincrona.
#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{
//Call the *Async method that starts the operation.
IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
DeviceInformation::FindAllAsync();
// Explicit construction. (Not recommended)
// Pass the IAsyncOperation to a task constructor.
// task<DeviceInformationCollection^> deviceEnumTask(deviceOp);
// Recommended:
auto deviceEnumTask = create_task(deviceOp);
// Call the task's .then member function, and provide
// the lambda to be invoked when the async operation completes.
deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
{
for(int i = 0; i < devices->Size; i++)
{
DeviceInformation^ di = devices->GetAt(i);
// Do something with di...
}
}); // end lambda
// Continue doing work or return...
}
L'attività creata e restituita dalla funzione task::then è nota come continuazione. L'argomento di input (in questo caso) per l'espressione lambda fornita dall'utente è il risultato che l'operazione dell'attività produce al termine dell'operazione. È lo stesso valore che verrebbe recuperato chiamando IAsyncOperation::GetResults se si usasse direttamente l'interfaccia IAsyncOperation.
Il metodo task::then restituisce immediatamente e il relativo delegato non viene eseguito finché il lavoro asincrono non viene completato correttamente. In questo esempio, se l'operazione asincrona causa la generazione di un'eccezione o termina nello stato annullato in seguito a una richiesta di annullamento, la continuazione non verrà mai eseguita. Successivamente, verrà descritto come scrivere le continuazioni eseguite anche se l'attività precedente è stata annullata o non riuscita.
Anche se si dichiara la variabile di attività nello stack locale, la relativa durata viene gestita in modo che non venga eliminata fino al completamento di tutte le operazioni e tutti i riferimenti a tale variabile esulano dall'ambito, anche se il metodo viene restituito prima del completamento delle operazioni.
Creazione di una catena di attività
Nella programmazione asincrona, è comune definire una sequenza di operazioni, nota anche come catene di attività, in cui ogni continuazione viene eseguita solo quando viene completata quella precedente. In alcuni casi, l'attività precedente (o precedente) produce un valore che la continuazione accetta come input. Usando il metodo task::then è possibile creare catene di attività in modo intuitivo e semplice. Il metodo restituisce un'attività<T> dove T è il tipo restituito della funzione lambda. È possibile comporre più continuazioni in una catena di attività: myTask.then(…).then(…).then(…);
Le catene di attività sono particolarmente utili quando una continuazione crea una nuova operazione asincrona; tale attività è nota come attività asincrona. Nell'esempio seguente viene illustrata una catena di attività con due continuazioni. L'attività iniziale acquisisce l'handle in un file esistente e, al termine dell'operazione, la prima continuazione avvia una nuova operazione asincrona per eliminare il file. Al termine dell'operazione, la seconda continuazione viene eseguita e restituisce un messaggio di conferma.
#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{
using namespace Windows::Storage;
StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
auto getFileTask = create_task(localFolder->GetFileAsync(fileName));
getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {
return storageFileSample->DeleteAsync();
}).then([](void) {
OutputDebugString(L"File deleted.");
});
}
Nell'esempio precedente vengono illustrati quattro punti importanti:
La prima continuazione converte l'oggetto IAsyncAction^ in una attività<void> e restituisce l'attività.
La seconda continuazione non esegue alcuna gestione degli errori e quindi accetta void e non task<void> come input. Si tratta di una continuazione basata su valori.
La seconda continuazione non viene eseguita fino al completamento dell'operazione DeleteAsync.
Poiché la seconda continuazione è basata sul valore, se l'operazione avviata dalla chiamata a DeleteAsync genera un'eccezione, la seconda continuazione non viene eseguita affatto.
Nota La creazione di una catena di attività è solo uno dei modi per usare la classe task per comporre operazioni asincrone. È anche possibile comporre operazioni usando operatori join e choice && e ||. Per ulteriori informazioni, vedere Parallelismo delle attività (runtime di concorrenza).
Tipi restituiti dalla funzione lambda e tipi restituiti di attività
In una continuazione dell'attività, il tipo restituito della funzione lambda viene sottoposto a wrapping in un oggetto attività. Se l'espressione lambda restituisce un valore double, quindi il tipo dell'attività di continuazione è task<double>. Tuttavia, l'oggetto attività è progettato in modo che non producano tipi restituiti annidati senza bisogno. Se un'espressione lambda restituisce un'attività IAsyncOperation<SyndicationFeed^>^, la continuazione restituisce un'attività<SyndicationFeed^>, non una attività<attività<SyndicationFeed^>> o attività<IAsyncOperation<SyndicationFeed^>^>^. Questo processo è noto come annullamento del wrapping asincrono e garantisce anche che l'operazione asincrona all'interno della continuazione venga completata prima che venga richiamata la continuazione successiva.
Nell'esempio precedente si noti che l'attività restituisce un'attività<void> anche se l'espressione lambda ha restituito un oggetto IAsyncInfo. La tabella seguente riepiloga le conversioni dei tipi che si verificano tra una funzione lambda e l'attività di inclusione:
tipo restituito dell'espressione lambda | .then tipo restituito |
---|---|
TResult | task<TResult> |
IAsyncOperation<TResult>^ | task<TResult> |
IAsyncOperationWithProgress<TResult, TProgress>^ | task<TResult> |
IAsyncAction^ | task<void> |
IAsyncActionWithProgress<TProgress>^ | task<void> |
task<TResult> | task<TResult> |
Annullamento delle attività
Spesso è consigliabile consentire all'utente di annullare un'operazione asincrona. In alcuni casi potrebbe essere necessario annullare un'operazione a livello di codice dall'esterno della catena di attività. Anche se ogni tipo restituito *Async ha un metodo Cancel che eredita da IAsyncInfo, è difficile esporlo a metodi esterni. Il modo preferito per supportare l'annullamento in una catena di attività consiste nell'usare un cancellation_token_source per creare un cancellation_token e quindi passare il token al costruttore dell'attività iniziale. Se viene creata un'attività asincrona con un token di annullamento e viene chiamato [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true), l'attività chiama automaticamente Cancel sull'operazione IAsync* e passa la richiesta di annullamento nella catena di continuazione. Lo pseudocodice seguente illustra l'approccio di base.
//Class member:
cancellation_token_source m_fileTaskTokenSource;
// Cancel button event handler:
m_fileTaskTokenSource.cancel();
// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
m_fileTaskTokenSource.get_token());
//getFileTask2.then ...
Quando un'attività viene annullata, viene propagata un'eccezione task_canceled nella catena di attività. Le continuazioni basate su valore non verranno semplicemente eseguite, ma le continuazioni basate su attività causeranno la generazione dell'eccezione quando viene chiamato task::get. Se si dispone di una continuazione di gestione degli errori, assicurarsi che intercetta in modo esplicito l'eccezione task_canceled. (Questa eccezione non è derivata da Platform::Exception).
L'annullamento è cooperativo, Se la continuazione esegue alcune operazioni a esecuzione prolungata oltre a richiamare un metodo UWP, è responsabilità dell'utente controllare periodicamente lo stato del token di annullamento e arrestare l'esecuzione se viene annullata. Dopo aver pulito tutte le risorse allocate nella continuazione, chiamare cancel_current_task per annullare l'attività e propagare l'annullamento a tutte le continuazioni basate sui valori che lo seguono. Ecco un altro esempio: è possibile creare una catena di attività che rappresenta il risultato di un'operazione FileSavePicker. Se l'utente seleziona il pulsante Annulla, il metodo IAsyncInfo::Cancel non viene chiamato. L'operazione ha esito positivo ma restituisce nullptr. La continuazione può testare il parametro di input e chiamare cancel_current_task se l'input è nullptr.
Per altre informazioni, vedere Cancellation in PPL.
Gestione degli errori in una catena di attività
Se si vuole che una continuazione venga eseguita anche se l'attività precedente è stata annullata o generata un'eccezione, impostare la continuazione su una continuazione basata su attività specificando l'input per la relativa funzione lambda come una attività<TResult> o un'attività<void> se l'espressione lambda dell'attività precedente restituisce un IAsyncAction^.
Per gestire gli errori e l'annullamento in una catena di attività, non è necessario creare tutte le attività di continuazione o racchiudere tutte le operazioni che potrebbero generare all'interno di un blocco try…catch
. È invece possibile aggiungere una continuazione basata su attività alla fine della catena e gestire tutti gli errori presenti. Qualsiasi eccezione, inclusa un'eccezione task_canceled, propaga la catena di attività e ignora tutte le continuazioni basate su valori, in modo che sia possibile gestirla nella continuazione basata su attività di gestione degli errori. È possibile riscrivere l'esempio precedente per usare una continuazione basata su attività di gestione degli errori:
#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{
using namespace Windows::Storage;
using namespace concurrency;
StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));
getFileTask.then([](StorageFile^ storageFileSample)
{
return storageFileSample->DeleteAsync();
})
.then([](task<void> t)
{
try
{
t.get();
// .get() didn' t throw, so we succeeded.
OutputDebugString(L"File deleted.");
}
catch (Platform::COMException^ e)
{
//Example output: The system cannot find the specified file.
OutputDebugString(e->Message->Data());
}
});
}
In una continuazione basata su attività viene chiamato task function membro::get per ottenere i risultati dell'attività. È comunque necessario chiamare task::get anche se l'operazione è un'operazione IAsyncAction che non produce alcun risultato perché task::get ottiene anche tutte le eccezioni che sono state trasportate all'attività. Se l'attività di input archivia un'eccezione, viene generata alla chiamata a task::get. Se non si chiama task::get o non si usa una continuazione basata su attività alla fine della catena o non si intercetta il tipo di eccezione generato, viene generata una unobserved_task_exception quando tutti i riferimenti all'attività sono stati eliminati.
Rilevare solo le eccezioni che è possibile gestire. Se l'app rileva un errore da cui non è possibile eseguire il ripristino, è preferibile lasciare che l'app si arresti in modo anomalo rispetto a consentire l'esecuzione in uno stato sconosciuto. Inoltre, in generale, non tentare di intercettare unobserved_task_exception stesso. Questa eccezione è destinata principalmente a scopi diagnostici. Quando viene generata unobserved_task_exception, in genere indica un bug nel codice. Spesso la causa è un'eccezione che deve essere gestita o un'eccezione irreversibile causata da un altro errore nel codice.
Gestione del contesto del thread
L'interfaccia utente di un'app UWP viene eseguita in un apartment a thread singolo (STA). Un'attività la cui espressione lambda restituisce un oggetto IAsyncAction o IAsyncOperation è compatibile con apartment. Se l'attività viene creata in STA, tutte le relative continuazioni verranno eseguite anche in esso per impostazione predefinita, a meno che non si specifichi diversamente. In altre parole, l'intera catena di attività eredita la consapevolezza dell'appartamento dall'attività padre. Questo comportamento consente di semplificare le interazioni con i controlli dell'interfaccia utente, accessibili solo da STA.
Ad esempio, in un'app UWP, nella funzione membro di qualsiasi classe che rappresenta una pagina XAML, si può compilare un controllo ListBox dall'interno di un metodo task::then senza dover usare l'oggetto Dispatcher.
#include <ppltasks.h>
void App::SetFeedText()
{
using namespace Windows::Web::Syndication;
using namespace concurrency;
String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
SyndicationClient^ client = ref new SyndicationClient();
auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));
create_task(feedOp).then([this] (SyndicationFeed^ feed)
{
m_TextBlock1->Text = feed->Title->Text;
});
}
Se un'attività non restituisce un IAsyncAction o IAsyncOperation, non è compatibile con apartment e, per impostazione predefinita, le relative continuazioni vengono eseguite nel primo thread in background disponibile.
È possibile eseguire l'override del contesto del thread predefinito per entrambi i tipi di attività usando l'overload di task::then che accetta un task_continuation_context. Ad esempio, in alcuni casi, potrebbe essere preferibile pianificare la continuazione di un'attività compatibile con apartment in un thread in background. In questo caso, è possibile passare task_continuation_context::use_arbitrary per pianificare il lavoro dell'attività sul thread successivo disponibile in un apartment a thread multipli. Ciò può migliorare le prestazioni della continuazione perché il lavoro non deve essere sincronizzato con altri lavori che si verificano nel thread dell'interfaccia utente.
L'esempio seguente illustra quando è utile specificare l'opzione task_continuation_context::use_arbitrary e mostra anche come il contesto di continuazione predefinito è utile per sincronizzare le operazioni simultanee in raccolte non thread-safe. In questo codice viene eseguito un ciclo attraverso un elenco di URL per i feed RSS e per ogni URL viene avviata un'operazione asincrona per recuperare i dati del feed. Non possiamo controllare l'ordine in cui vengono recuperati i feed e non ci interessa davvero. Al termine di ogni operazione RetrieveFeedAsync, la prima continuazione accetta l'oggetto SyndicationFeed^ e lo usa per inizializzare un oggetto definito dall'app FeedData^
. Poiché ognuna di queste operazioni è indipendente dalle altre, è possibile velocizzare le operazioni specificando il contesto di continuazione task_continuation_context::use_arbitrary. Tuttavia, dopo l'inizializzazione di ogni FeedData
oggetto, è necessario aggiungerlo a un oggetto Vector, che non è una raccolta thread-safe. Pertanto, creiamo una continuazione e specifichiamo [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) per garantire che tutte le chiamate a Append si verifichino nello stesso contesto di Application Single Threaded Apartment (ASTA). Poiché task_continuation_context::use_default è il contesto predefinito, non è necessario specificarlo in modo esplicito, ma è necessario farlo per maggiore chiarezza.
#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
using namespace concurrency;
SyndicationClient^ client = ref new SyndicationClient();
std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
{
// Create the async operation. feedOp is an
// IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
// but we don't handle progress in this example.
auto feedUri = ref new Uri(ref new String(url.c_str()));
auto feedOp = client->RetrieveFeedAsync(feedUri);
// Create the task object and pass it the async operation.
// SyndicationFeed^ is the type of the return value
// that the feedOp operation will eventually produce.
// Then, initialize a FeedData object by using the feed info. Each
// operation is independent and does not have to happen on the
// UI thread. Therefore, we specify use_arbitrary.
create_task(feedOp).then([this] (SyndicationFeed^ feed) -> FeedData^
{
return GetFeedData(feed);
}, task_continuation_context::use_arbitrary())
// Append the initialized FeedData object to the list
// that is the data source for the items collection.
// This all has to happen on the same thread.
// By using the use_default context, we can append
// safely to the Vector without taking an explicit lock.
.then([feedList] (FeedData^ fd)
{
feedList->Append(fd);
OutputDebugString(fd->Title->Data());
}, task_continuation_context::use_default())
// The last continuation serves as an error handler. The
// call to get() will surface any exceptions that were raised
// at any point in the task chain.
.then( [this] (task<void> t)
{
try
{
t.get();
}
catch(Platform::InvalidArgumentException^ e)
{
//TODO handle error.
OutputDebugString(e->Message->Data());
}
}); //end task chain
}); //end std::for_each
}
Le attività annidate, che sono nuove attività create all'interno di una continuazione, non ereditano la consapevolezza dell'attività iniziale.
Gestione degli aggiornamenti dello stato di avanzamento
I metodi che supportano IAsyncOperationWithProgress o IAsyncActionWithProgress forniscono periodicamente aggiornamenti dello stato di avanzamento mentre l'operazione è in corso, prima del completamento. La creazione di report sullo stato è indipendente dalla nozione di attività e continuazioni. È sufficiente specificare il delegato per la proprietà Progress dell'oggetto. Un uso tipico del delegato consiste nell'aggiornare un indicatore di stato nell'interfaccia utente.