Programação assíncrona em C++/CX
Observação
Este tópico existe para ajudar você a manter seu aplicativo em C++/CX. Entretanto, recomendamos usar C++/WinRT para novos aplicativos. C++/WinRT é uma projeção de linguagem C++17 completamente moderna e padrão para APIs do WinRT (Windows Runtime), implementada como uma biblioteca com base em cabeçalho e arquivo, projetada para fornecer acesso de primeira classe à API moderna do Windows.
Este artigo descreve a maneira recomendada de consumir métodos assíncronos em extensões de componentes do Visual C++ (C++/CX) ao usar a classe task
definida no namespace concurrency
em ppltasks.h.
Tipos assíncronos do Windows Runtime
O Windows Runtime apresenta um modelo bem definido para chamar métodos assíncronos e fornece os tipos necessários para consumir esses métodos. Se você não tiver familiaridade com o modelo assíncrono do Windows Runtime, leia Programação assíncrona antes de continuar a leitura deste artigo.
Embora seja possível consumir as APIs assíncronas do Windows Runtime diretamente em C++, a abordagem preferencial é usar a classe de tarefa e seus tipos e funções relacionados, que estão contidos no namespace de simultaneidade e definidos em <ppltasks.h>
. O concurrency::task é um tipo de uso geral, mas quando o parâmetro de compilador /ZW, necessário para aplicativos e componentes da Plataforma Universal do Windows (UWP), é usado, a classe de tarefa realiza o encapsulamento dos tipos assíncronos do Windows Runtime para que seja mais fácil:
realizar o encadeamento de várias operações assíncronas e síncronas em conjunto;
lidar com exceções em cadeias de tarefas;
executar o cancelamento em cadeias de tarefas;
garantir que as tarefas individuais estejam em execução no contexto ou no apartment de thread apropriado.
Este artigo fornece diretrizes básicas sobre como usar a classe de tarefa com as APIs assíncronas do Windows Runtime. Para obter a documentação mais completa sobre a classe de tarefa e seus métodos relacionados, incluindo create_task, veja Paralelismo de tarefa (runtime de simultaneidade).
Consumir uma operação assíncrona ao usar uma tarefa
O exemplo a seguir mostra como usar a classe de tarefa para consumir um método assíncrono que retorna uma interface IAsyncOperation e cuja operação produz um valor. Veja a seguir as etapas básicas:
Chame o método
create_task
e aprove o objeto IAsyncOperation^ para ele.Chame a função de membro task::then na tarefa e forneça uma função Lambda que será invocada quando a operação assíncrona for concluída.
#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...
}
A tarefa criada e retornada pela função task::then é conhecida como uma continuidade. O argumento de entrada (neste caso) para a função Lambda fornecida pelo usuário corresponde ao resultado que a operação de tarefa produz quando é concluída. O valor é semelhante ao que seria recuperado ao chamar IAsyncOperation::GetResults, se você estivesse usando a interface IAsyncOperation diretamente.
O método task::then realiza o retorno imediato e seu representante não é executado até que o trabalho assíncrono seja concluído com êxito. Neste exemplo, se a operação assíncrona causar a geração de uma exceção, ou o encerramento no estado cancelado, como resultado de uma solicitação de cancelamento, a continuidade nunca será executada. Posteriormente, descreveremos como escrever continuidades que serão executadas mesmo se a tarefa anterior tiver sido cancelada ou apresentado falhas.
Embora você declare a variável de tarefa na pilha local, ela gerencia a vida útil para que não seja excluída até que todas as operações sejam concluídas e todas as referências a ela saiam do escopo, mesmo se o método retornar antes da conclusão das operações.
Criar uma cadeia de tarefas
Na programação assíncrona, é comum definir uma sequência de operações, também conhecida como cadeias de tarefas, em que cada continuidade é executada somente quando a anterior é concluída. Em alguns casos, a tarefa anterior (ou antecedente) produz um valor que a continuidade aceita como entrada. Ao usar o método task::then, é possível criar cadeias de tarefas de maneira intuitiva e direta. O método retorna um resultado task<T>, em que T é o tipo de retorno da função Lambda. Você pode redigir múltiplas continuidades em uma cadeia de tarefas: myTask.then(…).then(…).then(…);
As cadeias de tarefas são especialmente úteis quando uma continuidade cria uma nova operação assíncrona e essa tarefa é conhecida como tarefa assíncrona. O exemplo apresentado a seguir ilustra uma cadeia de tarefas com duas continuidades. A tarefa inicial adquire o identificador para um arquivo existente e, quando essa operação é concluída, a primeira continuidade inicia uma nova operação assíncrona para excluir o arquivo. Quando essa operação for concluída, a segunda continuidade será executada e gerará uma mensagem de confirmação.
#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.");
});
}
O exemplo anterior ilustra quatro pontos importantes:
A primeira continuidade converte o objeto IAsyncAction^ em task<void> e retorna a tarefa.
A segunda continuidade não realiza tratamento de erro e, portanto, usa void e não task<void> como entrada. É uma continuidade baseada em valor.
A segunda continuidade não é executada até que a operação DeleteAsync seja concluída.
Como a segunda continuidade é baseada em valor, se a operação iniciada pela chamada para DeleteAsync gerar uma exceção, a segunda continuidade não será executada.
Observação: criar uma cadeia de tarefas é somente uma das maneiras de usar a classe de tarefa para redigir operações assíncronas. Além disso, é possível redigir operações ao usar os operadores de junção e de escolha && e ||. Para obter mais informações, veja Paralelismo de tarefa (runtime de simultaneidade).
Tipos de retornos da função Lambda e tipos de retornos da tarefa
Em uma continuidade de tarefa, o tipo de retorno da função Lambda é empacotado em um objeto de tarefa. Se a função Lambda retornar double, o tipo da tarefa de continuidade será task<double>. No entanto, o objeto de tarefa foi projetado para não produzir tipos de retornos aninhados desnecessariamente. Se uma função Lambda retornar um IAsyncOperation<SyndicationFeed^>^, a continuidade retornará task<SyndicationFeed^>, em vez de task<task<SyndicationFeed^>> ou de task<IAsyncOperation<SyndicationFeed^>^>^. Esse processo é conhecido como desempacotamento assíncrono e também garante que a operação assíncrona dentro da continuidade seja concluída antes que a próxima continuidade seja invocada.
No exemplo anterior, observe que a tarefa retorna task<void> mesmo que a função Lambda tenha retornado um objeto IAsyncInfo. A tabela a seguir resume as conversões de tipo que ocorrem entre uma função Lambda e a tarefa delimitadora:
Tipo de retorno do Lambda | Tipo de retorno do .then |
---|---|
TResult | task<TResult> |
IAsyncOperation<TResult>^ | task<TResult> |
IAsyncOperationWithProgress<TResult, TProgress>^ | task<TResult> |
IAsyncAction^ | task<void> |
IAsyncActionWithProgress<TProgress>^ | task<void> |
task<TResult> | task<TResult> |
Cancelando tarefas
Muitas vezes é uma boa ideia disponibilizar ao usuário a opção de cancelar uma operação assíncrona. Além disso, em alguns casos, pode ser necessário cancelar uma operação programaticamente de forma externa à cadeia de tarefas. Embora cada tipo de retorno *Async tenha um método Cancel herdado de IAsyncInfo, é estranho expô-lo a métodos externos. A maneira preferencial de oferecer suporte ao cancelamento em uma cadeia de tarefas é usar cancellation_token_source para criar um cancellation_token e, em seguida, aprovar o token para o construtor da tarefa inicial. Se uma tarefa assíncrona for criada com um token de cancelamento e [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true) for chamado, a tarefa chamará automaticamente Cancel na operação IAsync* e aprovará a solicitação de cancelamento para sua cadeia de continuidade. O pseudocódigo apresentado a seguir demonstra a abordagem básica.
//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 uma tarefa é cancelada, uma exceção task_canceled é propagada pela cadeia de tarefas. As continuidades baseadas em valor simplesmente não serão executadas, mas as continuidades baseadas em tarefas farão com que a exceção seja gerada quando task::get for chamado. Se você tiver uma continuidade de tratamento de erro, certifique-se de que ela capture explicitamente a exceção task_canceled. (Esta exceção não é derivada de Platform::Exception.)
O cancelamento é cooperativo. Se a sua continuidade realizar algum trabalho com execução prolongada além de somente invocar um método da UWP, será sua responsabilidade verificar periodicamente o estado do token de cancelamento e interromper a execução se ele for cancelado. Depois de limpar todos os recursos que foram alocados na continuidade chame cancel_current_task para cancelar essa tarefa e propagar o cancelamento para as continuações baseadas em valor que a seguem. Aqui está outro exemplo: é possível criar uma cadeia de tarefas que representa o resultado de uma operação FileSavePicker. Se o usuário escolher o botão Cancelar, o método IAsyncInfo::Cancel não será chamado. Em vez disso, a operação ocorre com êxito, mas retorna nullptr. A continuidade pode testar o parâmetro de entrada e chamar cancel_current_task se a entrada for nullptr.
Para obter mais informações, veja Cancelamento na PPL.
Tratamento de erros em uma cadeia de tarefas
Se você deseja que uma continuidade seja executada mesmo que o antecedente tenha sido cancelado ou gerado uma exceção, torne a continuidade uma continuidade baseada em tarefa ao especificar a entrada para sua função Lambda como task<TResult> ou task<void>, se a função Lambda da tarefa antecedente retornar IAsyncAction^.
Para tratar de erros e de cancelamentos em uma cadeia de tarefas, não é necessário fazer com que cada continuidade seja baseada em tarefas ou incluir todas as operações que possam ser geradas em um bloco try…catch
. Em vez disso, é possível adicionar uma continuidade baseada em tarefas no final da cadeia e tratar todos os erros contidos nela. Exceções, incluindo uma exceção task_canceled, se propagarão pela cadeia de tarefas e ignorarão as continuidades baseadas em valor, para que você possa tratá-las na continuidade baseada em tarefas de tratamento de erro. Podemos reescrever o exemplo anterior para usar uma continuidade baseada em tarefas de tratamento de erro:
#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());
}
});
}
Em uma continuidade baseada em tarefa, chamamos a função de membro task::get para obter os resultados da tarefa. Ainda temos que chamar task::get mesmo que a operação tenha sido uma IAsyncAction que não produz nenhum resultado porque task::get também obtém exceções que foram transportadas para a tarefa. Se a tarefa de entrada estiver armazenando uma exceção, ela será gerada na chamada para task::get. Se você não chamar task::get, não usar uma continuidade baseada em tarefa no final da cadeia, ou não capturar o tipo de exceção que foi gerado, então uma unobserved_task_exception será gerada quando todas as referências à tarefa tiverem sido excluídas.
Capture somente as exceções que você pode tratar. Se o aplicativo encontrar um erro do qual você não consegue se recuperar, é melhor deixar o aplicativo falhar do que continuar em execução em um estado desconhecido. Além disso, em geral, não tente capturar a própria unobserved_task_exception. Esta exceção se destina, principalmente, para fins de diagnóstico. Quando unobserved_task_exception é gerada, geralmente ela indica um bug no código. Muitas vezes a causa é uma exceção que deve ser tratada ou uma exceção irrecuperável causada por algum outro erro no código.
Gerenciar o contexto de thread
A interface do usuário de um aplicativo UWP é executada em um Single-Threaded Apartment (STA). Uma tarefa cuja função Lambda retorna IAsyncAction ou IAsyncOperation reconhece o apartment. Se a tarefa for criada no STA, todas as suas continuidades também serão executadas nele por padrão, a menos que você especifique o contrário. Em outras palavras, toda a cadeia de tarefas herda o reconhecimento de apartment da tarefa primária. Esse comportamento ajuda a simplificar as interações com controles de interface do usuário, que podem ser acessados somente do STA.
Por exemplo, em um aplicativo UWP, na função de membro de qualquer classe que representa uma página XAML, é possível preencher um controle ListBox em um método task::then sem a necessidade de usar o objeto 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 uma tarefa não retornar IAsyncAction ou IAsyncOperation, ela não terá reconhecimento de apartment e, por padrão, as continuidades serão executadas no primeiro thread em segundo plano disponível.
É possível substituir o contexto de thread padrão para qualquer tipo de tarefa ao usar a sobrecarga de task::then que tem um task_continuation_context. Por exemplo, em alguns casos, pode ser desejável agendar a continuidade de uma tarefa com reconhecimento de apartment em um thread em segundo plano. Nesse caso, é possível aprovar que task_continuation_context::use_arbitrary agende o trabalho da tarefa no próximo thread disponível em um apartment multithread. Isso pode melhorar o desempenho da continuidade porque seu trabalho não precisa ser sincronizado com outro trabalho que está acontecendo no thread da interface do usuário.
O exemplo a seguir demonstra quando é útil especificar a opção task_continuation_context::use_arbitrary e também mostra como o contexto de continuidade padrão é útil para sincronizar operações simultâneas em coleções que não são thread-safe. Neste código, percorremos uma lista de URLs para RSS feeds e, para cada URL, iniciamos uma operação assíncrona para recuperar os dados do feed. Não é possível controlar a ordem em que os feeds são recuperados, e isso realmente não tem importância. Quando cada operação RetrieveFeedAsync é concluída, a primeira continuidade aceita o objeto SyndicationFeed^ e o usa para inicializar um objeto FeedData^
definido pelo aplicativo. Como cada uma dessas operações é independente das outras, podemos potencialmente acelerar as coisas ao especificar o contexto de continuidade task_continuation_context::use_arbitrary. No entanto, após cada objeto FeedData
ser inicializado, temos que adicioná-lo a um Vetor, que não seja uma coleção thread-safe. Portanto, criamos uma continuidade e especificamos [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) para garantir que todas as chamadas para Acrescentar ocorrem no mesmo contexto do Single-Threaded Apartment de aplicativo (ASTA). Como task_continuation_context::use_default é o contexto padrão, não precisamos especificá-lo explicitamente, mas fazemos isso aqui por uma questão de clareza.
#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
}
As tarefas aninhadas, que são as novas tarefas criadas dentro de uma continuidade, não herdam o reconhecimento de apartment da tarefa inicial.
Como disponibilizar atualizações de progresso
Os métodos que oferecem suporte para IAsyncOperationWithProgress ou para IAsyncActionWithProgress fornecem atualizações de progresso periodicamente enquanto a operação está em andamento, ou seja, antes de ela ser concluída. Os relatórios de progresso são independentes da noção de tarefas e de continuidades. Você fornece somente o representante para a propriedade Progress. Um uso típico do representante é para atualizar uma barra de progresso na interface do usuário.