Programação assíncrona em C++ (aplicativos do Tempo de Execução do Windows)

Applies to Windows and Windows Phone

Este artigo descreve a maneira recomendada de consumir métodos assíncronos em extensões de componentes de Visual C++ (C++/CX) usando a classe task que está definida no namespace concurrency em ppltasks.h.

Também vale a pena ler Padrões e dicas de programação assíncrona no Hilo (aplicativos da Windows Store em C++ e XAML) para aprender como nós usamos o Tempo de Execução Concurrency para implementar operações assíncronas no Hilo, um aplicativo do Tempo de Execução do Windows em C++ e XAML.

Tipos assíncronos do Tempo de Execução do Windows

O Tempo de Execução do Windows possui um modelo bem definido para chamar métodos assíncronos e fornece os tipos que você precisa para consumir esses métodos. Caso você não conheça o modelo assíncrono do Tempo de Execução do Windows, leia Programação assíncrona antes de ler o restante deste artigo.

Apesar de ser possível consumir as APIs assíncronas do Tempo de Execução do Windows diretamente em C++, a abordagem recomendada é usar a task class e seus tipos e funções relacionados, que estão contidos no namespace concurrency e definidos em <ppltasks.h>. A classe concurrency::task é um tipo para fins gerais, mas quando a opção do compilador /ZW (necessária para aplicativos e componentes do Tempo de Execução do Windows) é usada, a classe da tarefa encapsula os tipos assíncronos do Tempo de Execução do Windows para facilitar as seguintes tarefas:

  • encadear diversas operações assíncronas e síncronas

  • lidar com exceções em cadeias de tarefas

  • executar cancelamentos em cadeias de tarefas

  • garantir que tarefas individuais sejam executadas no contexto ou apartment do thread apropriado

Este artigo fornece diretrizes básicas sobre como usar a classe task com as APIs assíncronas do Tempo de Execução do Windows. Para obter documentos mais completos sobre a task e os métodos relacionados, incluindo create_task, veja Paralelismo de tarefas (tempo de execução de simultaneidade). Para saber mais sobre como criar métodos assíncronos públicos para o consumo de JavaScript ou outras linguagens compatíveis com o Tempo de Execução do Windows, veja Criando operações assíncronas em C++ para aplicativos do Tempo de Execução do Windows.

Consumindo uma operação assíncrona usando uma tarefa

O exemplo a seguir mostra como usar a classe de tarefa para consumir um método async que retorna uma interface IAsyncOperation e cuja operação produz um valor. Estas são as etapas básicas:

  1. Chame o método create_task e passe a ele o objeto IAsyncOperation^.

  2. Chame a função de membro task::then na tarefa e forneça um lambda que será invocado 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 continuação. O argumento de entrada (neste caso) para a lambda fornecida pelo usuário é o resultado que a operação da tarefa produz quando é concluída. É o mesmo valor que seria recuperado chamando IAsyncOperation::GetResults se você estivesse usando a interface IAsyncOperation diretamente.

O método task::then é retornado imediatamente e seu delegado não é executado até que o trabalho assíncrono seja concluído com êxito. Neste exemplo, se a operação assíncrona gerar uma exceção ou for finalizada no estado cancelado como resultado de uma solicitação de cancelamento, a continuação nunca será executada. Posteriormente, descreveremos como gravar continuações que são executadas mesmo quando a tarefa anterior é cancelada ou apresenta falha.

Apesar de você declarar a variável da tarefa na pilha local, ela gerencia seu tempo de vida para que não seja excluída até que todas as suas operações sejam concluídas e todas as referências a ela saiam do escopo, mesmo que o método seja retornado antes da conclusão das operações.

Criando uma cadeia de tarefas

Na programação assíncrona, é comum definir uma sequência de operações, também conhecida como cadeias de tarefas, nas quais cada continuação é executada somente quando a anterior for concluída. Em alguns casos, a tarefa anterior (ou antecessora) produz um valor que a continuação aceita como entrada. Ao usar o método task::then, você pode criar cadeias de tarefas de forma intuitiva e direta; o método retorna uma task<T>, onde T é o tipo de retorno da função lambda. Você pode criar diversas continuações em uma cadeia de tarefas: myTask.then(…).then(…).then(…);

Cadeias de tarefas são especialmente úteis quando uma continuação cria uma nova operação assíncrona. Esse tipo de tarefa é conhecida como uma tarefa assíncrona. O exemplo a seguir ilustra uma cadeia de tarefas com duas continuações. A tarefa inicial adquire o identificador para um arquivo existente e, quando essa operação é concluída, a primeira continuação inicia uma nova operação assíncrona para excluir o arquivo. Quando essa operação é concluída, a segunda continuação é executada e gera uma mensagem de confirmação.



#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{    
    using namespace Windows::Storage;
    StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
    auto getFileTask = create_task(documentsFolder->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 continuação converte o objeto IAsyncAction^ em task<void> e retorna task.

  • A segunda continuação executa tratamento de erro e, portanto, considera void como entrada, não task<void>. Trata-se de uma continuação baseada em valores.

  • A segunda continuação não é executada até que a operação DeleteAsync seja concluída.

  • Como a segunda continuação é baseada em valores, se a operação iniciada pela chamada de DeleteAsync gerar uma exceção, a segunda continuação não é executada.

Observação  Criar uma cadeia de tarefas é apenas uma das formas de usar a classe task para criar operações assíncronas. Você também pode criar operações usando operadores de junção ou de opção && e ||. Para obter mais informações, consulte Paralelismo de tarefas (tempo de execução de simultaneidade).

Tipos de retorno da função lambda e tipos de retorno de tarefas

Em uma continuação de tarefa, o tipo de retorno da função lambda tem um objeto task como wrapper. Se a lambda retornar double, o tipo da tarefa de continuação é task<double>. No entanto, o objeto de tarefa foi criado para não produzir tipos de retorno desnecessariamente aninhados. Se uma lambda retornar IAsyncOperation<SyndicationFeed^>^, a continuação retornará task<SyndicationFeed^>, não task<task<SyndicationFeed^>> ou task<IAsyncOperation<SyndicationFeed^>^>^. Esse processo é conhecido como aninhamento assíncrono e também garante que a operação assíncrona dentro da continuação seja concluída antes de invocar a próxima continuação.

No exemplo anterior, observe que a tarefa retorna task<void>, mesmo que sua lambda tenha retornado um objeto IAsyncInfo. A tabela a seguir resume as conversos de tipo que ocorrem entre uma função lambda e a tarefa circunscrita:

tipo de retorno da lambda

tipo de retorno .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

Com frequência, é recomendável permitir que o usuário cancele uma operação assíncrona. Em alguns casos, você pode precisar cancelar uma operação de forma programática de fora da cadeia de tarefas. Apesar de cada tipo de retorno *Async ter um método Cancel herdado de IAsyncInfo, não é recomendável expô-lo a métodos externos. A forma recomendável para dar suporte a cancelamento em uma cadeia de tarefas é usar um cancellation_token_sourcepara criar um cancellation_token e, em seguida, passar 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 for chamado, a tarefa automaticamente chama Cancel na operação IAsync* e passa a solicitação de cancelamento pela cadeia da continuação. O pseudocódigo 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 continuações baseadas em valores simplesmente não são executadas, mas as continuações baseadas em tarefas geram a exceção quando task::get é chamado. Se você tem uma continuação de tratamento de erro, verifique se ela captura explicitamente a exceção task_canceled. (Essa exceção não é derivada de Platform::Exception.)

O cancelamento é cooperativa. Se a sua continuação executar tarefas de longa execução além de invocar um método do Tempo de Execução do Windows, você é responsável por verificar o estado do token de cancelamento periodicamente e interromper a execução se ela for cancelada. Depois de limpar todos os recursos alocados na continuação, chame cancel_current_task para cancelar a tarefa em questão e propagar o cancelamento para qualquer continuação baseada em valores posterior a ela. Outro exemplo: você pode criar uma cadeia de tarefas que represente o resultado de uma operação FileSavePicker. Se o usuário selecionar o botão Cancelar, o método IAsyncInfo::Cancel não será cancelado. Em vez disso, a operação é bem-sucedida, mas retorna nullptr. A continuação pode testar o parâmetro de entrada e chamar cancel_current_task se a entrada for nullptr.

Para obter mais informações, consulte Cancelamento no PPL

Tratamento de erro em uma cadeia de tarefas

Se você quiser que uma continuação seja executada mesmo que a antecessora tenha sido cancelada ou gerado uma exceção, transforme a continuação em uma continuação baseada em tarefas especificando a entrada de sua função lambda como task<TResult> ou task<void> se a lambda da tarefa antecessora retornar uma IAsyncAction^^.

Para tratar de erros e cancelamentos em uma cadeia de tarefas, não é necessário fazer com que todas as continuações sejam baseadas em tarefas ou circunscrever qualquer operação que possa gerar uma exceção em um bloco try…catch. Em vez disso, você pode adicionar uma continuação baseada em tarefas no final da cadeia e tratar de todos os erros lá. Qualquer exceção (incluindo uma exceção de task_canceled) será propagada pela cadeia de tarefas e ignorará todas as continuações baseadas em valores. Assim, você poderá tratar dela na continuação baseada em tarefas de tratamento de erro. Podemos recriar o exemplo anterior para usar uma continuação 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 continuação baseada em tarefas, nós chamamos a função de membro task::get para obter os resultados da tarefa. Nós ainda temos que chamar task::get mesmo que a operação seja uma IAsyncAction que não produza resultados, pois task::get também obtém as exceções que tenham sido transportadas para a tarefa. Se a tarefa de entrada estiver armazenando uma exceção, ela será gerada na chamada a task::get. Se você não chamar task::get ou não usar uma continuação baseada em tarefas no final da cadeia nem capturar o tipo de exceção que foi gerado, uma unobserved_task_exception será gerada quando todas as referências à tarefa forem excluídas.

Capture somente as exceções que você pode manipular. Se o seu aplicativo encontrar um erro do qual você não pode se recuperar, é melhor deixar que o aplicativo falhe em vez de permitir que ele continue sua execução em um estado desconhecido. No geral, não tente também capturar a exceção unobserved_task_exception em si. Essa exceção destina-se principalmente a fins diagnósticos. Quando a exceção unobserved_task_exception é gerada, ela geralmente indica um bug no código. Geralmente, ela é causada por uma exceção que deveria ser tratada ou por uma exceção irrecuperável provocada por algum outro erro no código.

Gerenciando o contexto do thread

A interface do usuário de um aplicativo do Tempo de Execução do Windows é executada em um STA (Single-Threaded Apartment). Uma tarefa na qual a lambda retorna IAsyncAction ou IAsyncOperation tem reconhecimento de apartment. Se a tarefa for criada no STA, todas as suas continuações também serão executadas nele por padrão, a não ser que você especifique o contrário. Em outras palavras, a cadeia de tarefas inteira herda o reconhecimento de apartment da tarefa pai. Esse comportamento ajuda a simplificar as interações com os controles da interface do usuário, que podem ser acessados somente pelo STA.

Por exemplo, em um aplicativo do Tempo de Execução do Windows, na função de membro de qualquer classe que represente uma página XAML, você pode preencher um controle ListBox de dentro de um método task::then sem 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 tem reconhecimento de apartment e, por padrão, suas continuações são executadas no primeiro thread de tela de fundo disponível.

Você pode substituir o contexto do thread padrão por qualquer tipo de tarefa usando a sobrecarga de task::then que usa task_continuation_context. Por exemplo, em alguns casos, pode ser recomendável agendar a continuação de uma tarefa com reconhecimento de apartment em um thread de tela de fundo. Nesse caso, você pode passar task_continuation_context::use_arbitrary para programar a execução da tarefa no próximo thread disponível em um Multi-Threaded Apartment. Isso pode melhorar o desempenho da continuação, pois a sua execução não precisa ser sincronizada com as demais execuções realizadas no thread da interface do usuário.

O exemplo a seguir demonstra quando é útil especificar a opção task_continuation_context::use_arbitrary, mostrando também como o contexto da continuação padrão é útil para a sincronização de operações simultâneas em coleções que não são thread-safe. Nesse código, executamos um loop por uma lista de URLs para RSS Feeds e, para cada URL, iniciamos uma operação assíncrona para recuperar os dados de feed. Não podemos controlar a ordem em que os feeds são recuperados. Isso não é importante. Quando cada operação RetrieveFeedAsync é concluída, a primeira continuação aceita o objeto SyndicationFeed^ e o usa para inicializar um objeto FeedData^ definido pelo aplicativo. Como cada uma dessas operações é independente das demais, é possível agilizar o processo especificando o contexto da continuação de task_continuation_context::use_arbitrary. No entanto, após a inicialização de cada objeto FeedData, precisamos adicioná-lo a um Vector, que não é uma coleção thread-safe. Portanto, criamos uma continuação e especificamos task_continuation_context::use_current para garantir que todas as chamadas para Append ocorram no mesmo contexto de ASTA. Como task_continuation_context::use_default é o contexto padrão, não precisamos especificá-lo explicitamente, mas fazemos isso aqui para fins de esclarecimento.


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


Tarefas aninhadas, que são tarefas novas criadas dentro de uma continuação, não herdam o reconhecimento de apartment da tarefa inicial.

Tratamento de atualizações de progresso

Métodos que oferecem suporte a IAsyncOperationWithProgress ou IAsyncActionWithProgress fornecem atualizações de progresso periodicamente enquanto a operação está em execução, até que ela seja concluída. A geração de relatórios de progresso independe da noção de tarefas e continuações. Basta fornecer o delegado para a propriedade Progress do objeto. O delegado é tipicamente usado para atualizar uma barra de progresso na interface do usuário.

Tópicos relacionados

Criando operações assíncronas em C++ para aplicativos da Windows Store
Referência da linguagem Visual C++
Mapa de aplicativos do Tempo de Execução do Windows em C++
Programação assíncrona
Paralelismo de tarefas (tempo de execução de simultaneidade)
task class

 

 

Mostrar:
© 2014 Microsoft