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:

  1. Chame o método create_task e aprove o objeto IAsyncOperation^ para ele.

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