Compartilhar via


Paralelismo de tarefa (runtime de simultaneidade)

No Runtime de Simultaneidade, uma tarefa é uma unidade de trabalho que executa um trabalho específico e normalmente é executada em paralelo com outras tarefas. Uma tarefa pode ser decomposta em tarefas adicionais e mais refinadas que são organizadas em um grupo de tarefas.

Você usa tarefas quando grava código assíncrono e deseja que alguma operação ocorra após a conclusão da operação assíncrona. Por exemplo, você pode usar uma tarefa para ler de forma assíncrona de um arquivo e, em seguida, usar outra tarefa, uma tarefa de continuação, que será explicada posteriormente neste documento, para processar os dados depois que eles ficarem disponíveis. Por outro lado, você pode usar grupos de tarefas para decompor o trabalho paralelo em partes menores. Por exemplo, suponha que você tenha um algoritmo recursivo que divide o trabalho restante em duas partições. Você pode usar grupos de tarefas para executar essas partições simultaneamente e aguardar a conclusão do trabalho dividido.

Dica

Quando você quiser aplicar a mesma rotina a cada elemento de uma coleção em paralelo, use um algoritmo paralelo, como concurrency::parallel_for, em vez de uma tarefa ou grupo de tarefas. Para obter mais informações sobre algoritmos paralelos, consulte Algoritmos paralelos.

Pontos Principais

  • Ao transmitir variáveis ​​para uma expressão lambda por referência, é necessário garantir que o tempo de vida da variável persista até que a tarefa seja concluída.

  • Use tarefas (a classe concurrency::task) ao gravar código assíncrono. A classe de tarefa usa o Windows ThreadPool como seu agendador, não o Runtime de Simultaneidade.

  • Use grupos de tarefas (a classe concurrency::task_group ou o algoritmo concurrency::parallel_invoke) quando quiser decompor o trabalho paralelo em partes menores e aguardar a conclusão dessas partes menores.

  • Use o método concurrency::task::then para criar continuações. Uma continuação é uma tarefa que é executada de forma assíncrona após a conclusão de outra tarefa. Você pode conectar qualquer número de continuações para formar uma cadeia de trabalho assíncrono.

  • Uma continuação baseada em tarefa sempre é agendada para execução quando a tarefa anterior é concluída, mesmo quando a tarefa anterior seja cancelada ou gere uma exceção.

  • Use concurrency::when_all para criar uma tarefa concluída após a conclusão de todos os membros de um conjunto de tarefas. Use concurrency::when_any para criar uma tarefa concluída após a conclusão de um membro de um conjunto de tarefas.

  • As tarefas e os grupos de tarefas podem participar do mecanismo de cancelamento da PPL (Biblioteca de Padrões Paralelos). Para obter mais informações, consulte Cancelamento na PPL.

  • Para saber como o runtime trata as exceções geradas por tarefas e grupos de tarefas, consulte Tratamento de Exceção.

Neste Documento

Usando Expressões Lambda

Devido à sintaxe sucinta, as expressões lambda são uma maneira comum de definir o trabalho executado por tarefas e grupos de tarefas. Aqui estão algumas dicas de uso:

  • Como as tarefas normalmente são executadas em threads em segundo plano, lembre-se do tempo de vida do objeto ao capturar variáveis em expressões lambda. Quando você captura uma variável por valor, uma cópia dessa variável é feita no corpo lambda. Quando você captura por referência, uma cópia não é feita. Portanto, verifique se o tempo de vida de qualquer variável que você capturar por referência é maior que o da tarefa que a usa.

  • Ao passar uma expressão lambda para uma tarefa, não capture variáveis alocadas na pilha por referência.

  • Seja explícito sobre as variáveis que você captura em expressões lambda para que você possa identificar o que está capturando por valor versus por referência. Por esse motivo, recomendamos que você não use as opções [=] ou [&] para expressões lambda.

Um padrão comum é quando uma tarefa em uma cadeia de continuação é atribuída a uma variável e outra tarefa lê essa variável. Você não pode capturar por valor porque cada tarefa de continuação conteria uma cópia diferente da variável. Para variáveis alocadas por pilha, também não é possível capturar por referência porque a variável pode não ser mais válida.

Para resolver esse problema, use um ponteiro inteligente, como std::shared_ptr, para encapsular a variável e passar o ponteiro inteligente por valor. Dessa forma, o objeto subjacente pode ser atribuído e lido e sobreviverá às tarefas que o usam. Use essa técnica mesmo quando a variável for um ponteiro ou um identificador contado por referência (^) para um objeto do Windows Runtime. Este é um exemplo básico:

// lambda-task-lifetime.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <string>

using namespace concurrency;
using namespace std;

task<wstring> write_to_string()
{
    // Create a shared pointer to a string that is 
    // assigned to and read by multiple tasks.
    // By using a shared pointer, the string outlives
    // the tasks, which can run in the background after
    // this function exits.
    auto s = make_shared<wstring>(L"Value 1");

    return create_task([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value.
        *s = L"Value 2";

    }).then([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value and return the string.
        *s = L"Value 3";
        return *s;
    });
}

int wmain()
{
    // Create a chain of tasks that work with a string.
    auto t = write_to_string();

    // Wait for the tasks to finish and print the result.
    wcout << L"Final value: " << t.get() << endl;
}

/* Output:
    Current value: Value 1
    Current value: Value 2
    Final value: Value 3
*/

Para obter mais informações sobre expressões lambda, consulte Expressões lambda.

A classe de tarefa

Você pode usar a classe concurrency::task para compor tarefas em um conjunto de operações dependentes. Esse modelo de composição é compatível com a noção de continuações. Uma continuação permite que o código seja executado quando a tarefa anterior ou antecessora for concluída. O resultado da tarefa antecessora é passado como a entrada para uma ou mais tarefas de continuação. Quando uma tarefa antecessora é concluída, todas as tarefas de continuação que estão esperando por ela são agendadas para execução. Cada tarefa de continuação recebe uma cópia do resultado da tarefa antecessora. Por sua vez, essas tarefas de continuação também podem ser tarefas antecessora para outras continuações, criando, assim, uma cadeia de tarefas. As continuações ajudam você a criar cadeias arbitrárias de tarefas que têm dependências específicas entre elas. Além disso, uma tarefa pode participar do cancelamento antes que uma tarefa seja iniciada ou de maneira cooperativa enquanto estiver em execução. Para saber mais sobre esse modelo de cancelamento, confira Cancelamento no PPL.

task é uma classe de modelo. O parâmetro de tipo T é o tipo do resultado produzido pela tarefa. Esse tipo poderá ser void se a tarefa não retornar um valor. T não pode usar o modificador const.

Ao criar uma tarefa, você fornece uma função de trabalho que executa o corpo da tarefa. Essa função de trabalho vem na forma de uma função lambda, ponteiro de função ou objeto de função. Para aguardar a conclusão de uma tarefa sem obter o resultado, chame o método concurrency::task::wait. O método task::wait retorna um valor concurrency::task_status que descreve se a tarefa foi concluída ou cancelada. Para obter o resultado da tarefa, chame o método concurrency::task::get. Esse método chama task::wait para aguardar a conclusão da tarefa e, portanto, bloqueia a execução do thread atual até que o resultado esteja disponível.

O exemplo a seguir mostra como criar uma tarefa, aguardar seu resultado e exibir seu valor. Os exemplos nesta documentação usam funções lambda porque fornecem uma sintaxe mais sucinta. No entanto, você também pode usar ponteiros de função e objetos de função ao usar tarefas.

// basic-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Create a task.
    task<int> t([]()
    {
        return 42;
    });

    // In this example, you don't necessarily need to call wait() because
    // the call to get() also waits for the result.
    t.wait();

    // Print the result.
    wcout << t.get() << endl;
}

/* Output:
    42
*/

Ao usar a função concurrency::create_task , você pode usar a palavra-chave auto em vez de declarar o tipo. Por exemplo, considere esse código que cria e imprime a matriz de identidade:

// create-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <string>
#include <iostream>
#include <array>

using namespace concurrency;
using namespace std;

int wmain()
{
    task<array<array<int, 10>, 10>> create_identity_matrix([]
    {
        array<array<int, 10>, 10> matrix;
        int row = 0;
        for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
        {
            fill(begin(matrixRow), end(matrixRow), 0);
            matrixRow[row] = 1;
            row++;
        });
        return matrix;
    });

    auto print_matrix = create_identity_matrix.then([](array<array<int, 10>, 10> matrix)
    {
        for_each(begin(matrix), end(matrix), [](array<int, 10>& matrixRow) 
        {
            wstring comma;
            for_each(begin(matrixRow), end(matrixRow), [&comma](int n) 
            {
                wcout << comma << n;
                comma = L", ";
            });
            wcout << endl;
        });
    });

    print_matrix.wait();
}
/* Output:
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0
    0, 1, 0, 0, 0, 0, 0, 0, 0, 0
    0, 0, 1, 0, 0, 0, 0, 0, 0, 0
    0, 0, 0, 1, 0, 0, 0, 0, 0, 0
    0, 0, 0, 0, 1, 0, 0, 0, 0, 0
    0, 0, 0, 0, 0, 1, 0, 0, 0, 0
    0, 0, 0, 0, 0, 0, 1, 0, 0, 0
    0, 0, 0, 0, 0, 0, 0, 1, 0, 0
    0, 0, 0, 0, 0, 0, 0, 0, 1, 0
    0, 0, 0, 0, 0, 0, 0, 0, 0, 1
*/

Você pode usar a função create_task para criar a operação equivalente.

auto create_identity_matrix = create_task([]
{
    array<array<int, 10>, 10> matrix;
    int row = 0;
    for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
    {
        fill(begin(matrixRow), end(matrixRow), 0);
        matrixRow[row] = 1;
        row++;
    });
    return matrix;
});

Se uma exceção for gerada durante a execução de uma tarefa, o runtime realizará marshals dessa exceção na chamada subsequente para task::get ou task::waitpara uma continuação baseada em tarefas. Para obter mais informações sobre o mecanismo de tratamento de exceções da tarefa, consulte Tratamento de Exceção.

Para obter um exemplo que usa task, concurrency::task_completion_event, cancelamento, consulte Passo a passo: Conectando-se usando tarefas e solicitações HTTP XML. (A classe task_completion_event está descrita posteriormente neste documento.)

Dica

Para saber detalhes específicos de tarefas em aplicativos UWP, consulte Programação assíncrona em C++ e Criando operações assíncronas em C++ para aplicativos UWP.

Tarefas de continuação

Na programação assíncrona, é muito comum para uma operação assíncrona, após a conclusão, invocar uma segunda operação e passar dados para ela. Tradicionalmente, isso é feito usando os métodos de retorno de chamada. No Runtime de Simultaneidade, a mesma funcionalidade é fornecida pelas tarefas de continuação. Uma tarefa de continuação (também conhecida como uma continuação) é uma tarefa assíncrona invocada por outra tarefa, que é conhecida como a antecessora, quando a antecessora conclui. Usando continuações, você pode:

  • Passe dados da antecessora para a continuação.

  • Especifique as condições precisas sob a qual a continuação é invocada ou não invocada.

  • Cancele uma continuação antes que ela comece ou cooperativamente enquanto ela estiver em execução.

  • Forneça dicas sobre como a continuação deve ser agendada. Isso se aplica apenas a aplicativos UWP (Plataforma Universal do Windows). Para obter mais informações, consulte Criar operações assíncronas no C++ para aplicativos UWP.)

  • Invoque várias continuações da mesma antecessora.

  • Invoque uma continuação quando todas ou quaisquer diversas antecessoras for concluída.

  • Encadeie continuações uma após a outra para qualquer comprimento.

  • Use uma continuação para manipular exceções lançadas pela antecessora.

Esses recursos permitem executar uma ou mais tarefas quando a primeira tarefa for concluída. Por exemplo, você pode criar uma continuação que compacta um arquivo após a primeira tarefa lê-lo do disco.

O exemplo a seguir modifica o anterior para usar o método concurrency::task::then para agendar uma continuação que imprime o valor da tarefa anterior quando disponível.

// basic-continuation.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    {
        return 42;
    });

    t.then([](int result)
    {
        wcout << result << endl;
    }).wait();

    // Alternatively, you can chain the tasks directly and
    // eliminate the local variable.
    /*create_task([]() -> int
    {
        return 42;
    }).then([](int result)
    {
        wcout << result << endl;
    }).wait();*/
}

/* Output:
    42
*/

Você pode encadear e aninhar tarefas em qualquer comprimento. Uma tarefa também pode ter várias continuações. O exemplo a seguir ilustra uma cadeia de continuação básica que incrementa o valor da tarefa anterior três vezes.

// continuation-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    { 
        return 0;
    });
    
    // Create a lambda that increments its input value.
    auto increment = [](int n) { return n + 1; };

    // Run a chain of continuations and print the result.
    int result = t.then(increment).then(increment).then(increment).get();
    wcout << result << endl;
}

/* Output:
    3
*/

Uma continuação também pode retornar outra tarefa. Se não houver cancelamento, essa tarefa será executada antes da continuação subsequente. Essa técnica é conhecida como cancelamento de quebra de linha assíncrono. O cancelamento de quebra de linha assíncrono é útil quando você deseja executar trabalho adicional em segundo plano, mas não deseja que a tarefa atual bloqueie o thread atual. (Isso é comum em aplicativos UWP, onde as continuações podem ser executadas no thread da interface do usuário). O exemplo a seguir mostra três tarefas. A primeira tarefa retorna outra tarefa que é executada antes de uma tarefa de continuação.

// async-unwrapping.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]()
    {
        wcout << L"Task A" << endl;

        // Create an inner task that runs before any continuation
        // of the outer task.
        return create_task([]()
        {
            wcout << L"Task B" << endl;
        });
    });
  
    // Run and wait for a continuation of the outer task.
    t.then([]()
    {
        wcout << L"Task C" << endl;
    }).wait();
}

/* Output:
    Task A
    Task B
    Task C
*/

Importante

Quando uma continuação de uma tarefa retorna uma tarefa aninhada do tipo N, a tarefa resultante tem o tipo N, não task<N> e é concluída quando a tarefa aninhada é concluída. Em outras palavras, a continuação executa o cancelamento de quebra de linha da tarefa aninhada.

Continuações baseadas em valor versus baseadas em tarefas

Dado um objeto task cujo tipo de retorno é T, você pode fornecer um valor de tipo T ou task<T> para suas tarefas de continuação. Uma continuação que usa o tipo T é conhecida como continuação baseada em valor. Uma continuação baseada no valor é agendada para execução quando a tarefa antecessora é concluída sem erro e não é cancelada. Uma continuação que usa o tipo task<T> como parâmetro é conhecida como continuação baseada em tarefa. Uma continuação baseada em tarefa sempre é agendada para execução quando a tarefa anterior é concluída, mesmo quando a tarefa anterior seja cancelada ou gere uma exceção. Em seguida, você pode chamar task::get para obter o resultado da tarefa antecessora. Se a tarefa antecessora foi cancelada, task::get gera concurrency::task_canceled. Se a tarefa antecessora gerar uma exceção, task::get gera novamente essa exceção. Uma continuação baseada em tarefa não é marcada como cancelada quando sua tarefa antecessora é cancelada.

Compondo tarefas

Esta seção descreve as funções concurrency::when_all e concurrency::when_any que podem ajudar a compor várias tarefas para implementar padrões comuns.

A função when_all

A função when_all produz uma tarefa que é concluída após a conclusão de um conjunto de tarefas. Essa função retorna um objeto std::vector que contém o resultado de cada tarefa no conjunto. O exemplo básico a seguir usa when_all para criar uma tarefa que representa a conclusão de três outras tarefas.

// join-tasks.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<void>, 3> tasks = 
    {
        create_task([] { wcout << L"Hello from taskA." << endl; }),
        create_task([] { wcout << L"Hello from taskB." << endl; }),
        create_task([] { wcout << L"Hello from taskC." << endl; })
    };

    auto joinTask = when_all(begin(tasks), end(tasks));

    // Print a message from the joining thread.
    wcout << L"Hello from the joining thread." << endl;

    // Wait for the tasks to finish.
    joinTask.wait();
}

/* Sample output:
    Hello from the joining thread.
    Hello from taskA.
    Hello from taskC.
    Hello from taskB.
*/

Observação

As tarefas que você passa para when_all devem ser uniformes. Em outras palavras, todos eles devem retornar o mesmo tipo.

Você também pode usar a sintaxe && para produzir uma tarefa que é concluída após a conclusão de um conjunto de tarefas, conforme mostrado no exemplo a seguir.

auto t = t1 && t2; // same as when_all

É comum usar uma continuação junto com when_all para executar uma ação após o término de um conjunto de tarefas. O exemplo a seguir modifica o anterior para imprimir a soma de três tarefas que produzem um resultado int.

// Start multiple tasks.
array<task<int>, 3> tasks =
{
    create_task([]() -> int { return 88; }),
    create_task([]() -> int { return 42; }),
    create_task([]() -> int { return 99; })
};

auto joinTask = when_all(begin(tasks), end(tasks)).then([](vector<int> results)
{
    wcout << L"The sum is " 
          << accumulate(begin(results), end(results), 0)
          << L'.' << endl;
});

// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;

// Wait for the tasks to finish.
joinTask.wait();

/* Output:
    Hello from the joining thread.
    The sum is 229.
*/

Neste exemplo, você também pode especificar task<vector<int>> para produzir uma continuação baseada em tarefas.

Se qualquer tarefa em um conjunto de tarefas for cancelada ou gerar uma exceção, when_all será concluído imediatamente e não aguardará a conclusão das tarefas restantes. Se uma exceção for lançada, o runtime gerará novamente a exceção quando você chamar task::get ou task::wait o objeto de tarefa que when_all retorna. Se mais de uma tarefa for gerada, o runtime escolherá uma delas. Portanto, verifique se você observa todas as exceções após a conclusão de todas as tarefas; uma exceção de tarefa sem tratamento faz com que o aplicativo seja encerrado.

Aqui está uma função de utilitário que você pode usar para garantir que o programa observe todas as exceções. Para cada tarefa no intervalo fornecido, observe_all_exceptions dispara qualquer exceção que ocorreu para ser gerada novamente e, em seguida, absorve essa exceção.

// Observes all exceptions that occurred in all tasks in the given range.
template<class T, class InIt> 
void observe_all_exceptions(InIt first, InIt last) 
{
    std::for_each(first, last, [](concurrency::task<T> t)
    {
        t.then([](concurrency::task<T> previousTask)
        {
            try
            {
                previousTask.get();
            }
            // Although you could catch (...), this demonstrates how to catch specific exceptions. Your app
            // might handle different exception types in different ways.
            catch (Platform::Exception^)
            {
                // Swallow the exception.
            }
            catch (const std::exception&)
            {
                // Swallow the exception.
            }
        });
    });
}

Considere um aplicativo UWP que usa C++ e XAML e grava um conjunto de arquivos em disco. O exemplo a seguir mostra como usar when_all e observe_all_exceptions para garantir que o programa observe todas as exceções.

// Writes content to files in the provided storage folder.
// The first element in each pair is the file name. The second element holds the file contents.
task<void> MainPage::WriteFilesAsync(StorageFolder^ folder, const vector<pair<String^, String^>>& fileContents)
{
    // For each file, create a task chain that creates the file and then writes content to it. Then add the task chain to a vector of tasks.
    vector<task<void>> tasks;
    for (auto fileContent : fileContents)
    {
        auto fileName = fileContent.first;
        auto content = fileContent.second;

        // Create the file. The CreationCollisionOption::FailIfExists flag specifies to fail if the file already exists.
        tasks.emplace_back(create_task(folder->CreateFileAsync(fileName, CreationCollisionOption::FailIfExists)).then([content](StorageFile^ file)
        {
            // Write its contents.
            return create_task(FileIO::WriteTextAsync(file, content));
        }));
    }

    // When all tasks finish, create a continuation task that observes any exceptions that occurred.
    return when_all(begin(tasks), end(tasks)).then([tasks](task<void> previousTask)
    {
        task_status status = completed;
        try
        {
            status = previousTask.wait();
        }
        catch (COMException^ e)
        {
            // We'll handle the specific errors below.
        }
        // TODO: If other exception types might happen, add catch handlers here.

        // Ensure that we observe all exceptions.
        observe_all_exceptions<void>(begin(tasks), end(tasks));

        // Cancel any continuations that occur after this task if any previous task was canceled.
        // Although cancellation is not part of this example, we recommend this pattern for cases that do.
        if (status == canceled)
        {
            cancel_current_task();
        }
    });
}
Para executar esse exemplo
  1. Em MainPage.xaml, adicione um controle Button.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. Em MainPage.xaml.h, adicione essas declarações de encaminhamento à seção private da declaração de classe MainPage.
void Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
concurrency::task<void> WriteFilesAsync(Windows::Storage::StorageFolder^ folder, const std::vector<std::pair<Platform::String^, Platform::String^>>& fileContents);
  1. Em MainPage.xaml.cpp, implemente o manipulador de eventos Button_Click.
// A button click handler that demonstrates the scenario.
void MainPage::Button_Click(Object^ sender, RoutedEventArgs^ e)
{
    // In this example, the same file name is specified two times. WriteFilesAsync fails if one of the files already exists.
    vector<pair<String^, String^>> fileContents;
    fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 1")));
    fileContents.emplace_back(make_pair(ref new String(L"file2.txt"), ref new String(L"Contents of file 2")));
    fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 3")));

    Button1->IsEnabled = false; // Disable the button during the operation.
    WriteFilesAsync(ApplicationData::Current->TemporaryFolder, fileContents).then([this](task<void> previousTask)
    {
        try
        {
            previousTask.get();
        }
        // Although cancellation is not part of this example, we recommend this pattern for cases that do.
        catch (const task_canceled&)
        {
            // Your app might show a message to the user, or handle the error in some other way.
        }

        Button1->IsEnabled = true; // Enable the button.
    });
}
  1. Em MainPage.xaml.cpp, implemente WriteFilesAsync conforme mostrado no exemplo.

Dica

when_all é uma função sem bloqueio que produz task como resultado. Ao contrário de task::wait, é seguro chamar essa função em um aplicativo UWP no thread ASTA (Application STA).

A função when_any

A função when_any produz uma tarefa que é concluída quando a primeira tarefa em um conjunto de tarefas é concluída. Essa função retorna um objeto std::pair que contém o resultado da tarefa concluída e o índice dessa tarefa no conjunto.

A função when_any é especialmente útil nos cenários a seguir:

  • Operações redundantes. Considere um algoritmo ou uma operação que possam ser executados de várias maneiras. Você pode usar a função when_any para selecionar a operação que termina primeiro e então cancelar as operações restantes.

  • Operações intercaladas. Você pode iniciar várias operações que todos devem concluir e usar a função when_any para processar resultados à medida que cada operação termina. Após uma operação ser concluída, você poderá começar uma ou mais tarefas adicionais.

  • Operações controladas. Você pode usar a função when_any para estender o cenário anterior ao limitar o número de operações simultâneas.

  • Operações expiradas. Você pode usar a função when_any para selecionar entre uma ou mais tarefas e uma tarefa que termine após uma hora específica.

Assim como acontece com when_all, é comum usar uma continuação que precisa de when_any para executar a ação quando a primeira em um conjunto de tarefas for concluída. O exemplo básico a seguir usa when_any para criar uma tarefa que é concluída quando a primeira de três outras tarefas é concluída.

// select-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<int>, 3> tasks = {
        create_task([]() -> int { return 88; }),
        create_task([]() -> int { return 42; }),
        create_task([]() -> int { return 99; })
    };

    // Select the first to finish.
    when_any(begin(tasks), end(tasks)).then([](pair<int, size_t> result)
    {
        wcout << "First task to finish returns "
              << result.first
              << L" and has index "
              << result.second
              << L'.' << endl;
    }).wait();
}

/* Sample output:
    First task to finish returns 42 and has index 1.
*/

Neste exemplo, você também pode especificar task<pair<int, size_t>> para produzir uma continuação baseada em tarefas.

Observação

Assim como acontece com when_all, as tarefas passadas para when_any devem retornar o mesmo tipo.

Você também pode usar a sintaxe || para produzir uma tarefa que é concluída após a conclusão da primeira tarefa em um conjunto de tarefas, conforme mostrado no exemplo a seguir.

auto t = t1 || t2; // same as when_any

Dica

Assim como acontece com when_all, when_any não está bloqueando e é seguro chamar um aplicativo UWP no thread ASTA.

Execução de tarefa atrasada

Às vezes é necessário atrasar a execução de uma tarefa até que uma condição seja atendida, ou iniciar uma tarefa em resposta a um evento externo. Por exemplo, na programação assíncrona, talvez seja necessário iniciar uma tarefa em resposta a um evento de conclusão de E/S.

Duas maneiras de fazer isso são usar uma continuação ou iniciar uma tarefa e aguardar um evento dentro da função de trabalho da tarefa. No entanto, há casos em que não é possível usar uma dessas técnicas. Por exemplo, para criar uma continuação, você deve ter a tarefa antecessora. No entanto, se você não tiver a tarefa antecessora, poderá criar um evento de conclusão de tarefa e uma cadeia posterior desse evento de conclusão para a tarefa antecessora quando ela ficar disponível. Além disso, como uma tarefa de espera também bloqueia um thread, você pode usar eventos de conclusão de tarefa para executar o trabalho quando uma operação assíncrona for concluída e, assim, liberar um thread.

A classe concurrency::task_completion_event ajuda a simplificar essa composição de tarefas. Assim como na classe task, o parâmetro de tipo T é o tipo do resultado produzido pela tarefa. Esse tipo poderá ser void se a tarefa não retornar um valor. T não pode usar o modificador const. Normalmente, um objeto task_completion_event é fornecido a um thread ou tarefa que o sinalizará quando o valor ficar disponível. Ao mesmo tempo, uma ou mais tarefas são definidas como ouvintes desse evento. Quando o evento é definido, as tarefas do ouvinte são concluídas e suas continuações são agendadas para execução.

Para obter um exemplo que usa task_completion_event para implementar uma tarefa concluída após um atraso, consulte Como criar uma tarefa que seja concluída após um atraso.

Grupos de tarefas

Um grupo de tarefas organiza uma coleção de tarefas. Grupos de tarefas efetuam push de tarefas para uma fila de roubo de trabalho. O agendador remove as tarefas dessa fila e as executa nos recursos de computação disponíveis. Depois de adicionar tarefas a um grupo de tarefas, você pode esperar que todas as tarefas sejam concluídas ou cancelar as tarefas que ainda não foram iniciadas.

O PPL usa as classes concurrency::task_group e concurrency::structured_task_group para representar grupos de tarefas e a classe concurrency::task_handle para representar as tarefas executadas nesses grupos. A classe task_handle encapsula o código que executa o trabalho. Assim como a classe task, a função de trabalho vem na forma de uma função lambda, ponteiro de função ou objeto de função. Normalmente, você não precisa trabalhar diretamente com objetos task_handle. Em vez disso, você passa funções de trabalho para um grupo de tarefas e o grupo de tarefas cria e gerencia os objetos task_handle.

O PPL divide grupos de tarefas nessas duas categorias: grupos de tarefas não estruturados e grupos de tarefas estruturados. O PPL usa a classe task_group para representar grupos de tarefas não estruturados e a classe structured_task_group para representar grupos de tarefas estruturados.

Importante

O PPL também define o algoritmo concurrency::parallel_invoke, que usa a classe structured_task_group para executar um conjunto de tarefas em paralelo. Como o algoritmo parallel_invoke tem uma sintaxe mais sucinta, recomendamos que você o use em vez da classe structured_task_group quando puder. O tópico Algoritmos Paralelos descreve parallel_invoke em detalhes.

Use parallel_invoke quando você tiver várias tarefas independentes que deseja executar ao mesmo tempo e aguarde a conclusão de todas as tarefas antes de prosseguir. Essa técnica é frequentemente conhecida como paralelismo de bifurcação e junção. Use task_group quando tiver várias tarefas independentes que deseja executar ao mesmo tempo, mas deseja aguardar a conclusão das tarefas posteriormente. Por exemplo, você pode adicionar tarefas a um objeto task_group e aguardar a conclusão das tarefas em outra função ou de outro thread.

Os grupos de tarefas dão suporte ao conceito de cancelamento. O cancelamento permite que você sinalize para todas as tarefas ativas que você deseja cancelar a operação geral. O cancelamento também impede que as tarefas que ainda não começaram sejam iniciadas. Para saber mais sobre cancelamento, confira Cancelamento no PPL.

O runtime também fornece um modelo de tratamento de exceções que permite gerar uma exceção de uma tarefa e manipular essa exceção ao aguardar a conclusão do grupo de tarefas associado. Para mais informações sobre esse modelo do tratamento de exceção, confira Tratamento de Exceção.

Comparando task_group com structured_task_group

Embora seja recomendável usar task_group ou parallel_invoke em vez da classe structured_task_group, há casos em que você deseja usar structured_task_group, por exemplo, quando você grava um algoritmo paralelo que executa um número variável de tarefas ou requer suporte para cancelamento. Esta seção explica as diferenças entre as classes task_group e structured_task_group.

A classe task_group é segura para threads. Portanto, você pode adicionar tarefas a um objeto task_group de vários threads e aguardar ou cancelar um objeto task_group de vários threads. A construção e a destruição de um objeto structured_task_group devem ocorrer no mesmo escopo lexical. Além disso, todas as operações em um objeto structured_task_group devem ocorrer no mesmo thread. A exceção a essa regra são os métodos concurrency::structured_task_group::cancel e concurrency::structured_task_group::is_canceling. Uma tarefa filho pode chamar esses métodos para cancelar o grupo de tarefas pai ou verificar o cancelamento a qualquer momento.

Você pode executar tarefas adicionais em um objeto task_group depois de chamar o método concurrency::task_group::wait ou concurrency::task_group::run_and_wait. Por outro lado, se você executar tarefas adicionais em um objeto structured_task_group depois de chamar os métodos concurrency::structured_task_group::wait ou concurrency::structured_task_group::run_and_wait, o comportamento será indefinido.

Como a classe structured_task_group não sincroniza entre threads, ela tem menos sobrecarga de execução do que a classe task_group. Portanto, se o problema não exigir que você agende trabalho de vários threads e não possa usar o algoritmo parallel_invoke, a classe structured_task_group poderá ajudá-lo a gravar um código com melhor desempenho.

Se você usar um objeto structured_task_group dentro de outro objeto structured_task_group, o objeto interno deverá ser concluído e destruído antes que o objeto externo seja concluído. A classe task_group não exige que os grupos de tarefas aninhados sejam concluídos antes que o grupo externo seja concluído.

Grupos de tarefas não estruturados e grupos de tarefas estruturados trabalham com identificadores de tarefas de diferentes maneiras. Você pode passar funções de trabalho diretamente para um objeto task_group; o objeto task_group criará e gerenciará o identificador de tarefa para você. A classe structured_task_group exige que você gerencie um objeto task_handle para cada tarefa. Cada objeto task_handle deve permanecer válido durante todo o tempo de vida do objeto structured_task_group associado. Use a função concurrency::make_task para criar um objeto task_handle, conforme mostrado no exemplo básico a seguir:

// make-task-structure.cpp
// compile with: /EHsc
#include <ppl.h>

using namespace concurrency;

int wmain()
{
   // Use the make_task function to define several tasks.
   auto task1 = make_task([] { /*TODO: Define the task body.*/ });
   auto task2 = make_task([] { /*TODO: Define the task body.*/ });
   auto task3 = make_task([] { /*TODO: Define the task body.*/ });

   // Create a structured task group and run the tasks concurrently.

   structured_task_group tasks;

   tasks.run(task1);
   tasks.run(task2);
   tasks.run_and_wait(task3);
}

Para gerenciar identificadores de tarefa para casos em que você tem um número variável de tarefas, use uma rotina de alocação de pilha, como _malloca ou uma classe de contêiner, como std::vector.

task_group e structured_task_group dão suporte ao cancelamento. Para saber mais sobre cancelamento, confira Cancelamento no PPL.

Exemplo

O exemplo básico a seguir mostra como trabalhar com grupos de tarefas. Este exemplo usa o algoritmo parallel_invoke para executar duas tarefas simultaneamente. Cada tarefa adiciona subtarefas a um objeto task_group. Observe que a classe task_group permite que várias tarefas adicionem tarefas a ela simultaneamente.

// using-task-groups.cpp
// compile with: /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>

using namespace concurrency;
using namespace std;

// Prints a message to the console.
template<typename T>
void print_message(T t)
{
   wstringstream ss;
   ss << L"Message from task: " << t << endl;
   wcout << ss.str(); 
}

int wmain()
{  
   // A task_group object that can be used from multiple threads.
   task_group tasks;

   // Concurrently add several tasks to the task_group object.
   parallel_invoke(
      [&] {
         // Add a few tasks to the task_group object.
         tasks.run([] { print_message(L"Hello"); });
         tasks.run([] { print_message(42); });
      },
      [&] {
         // Add one additional task to the task_group object.
         tasks.run([] { print_message(3.14); });
      }
   );

   // Wait for all tasks to finish.
   tasks.wait();
}

O demonstrado a seguir é uma saída de exemplo para esse exemplo:

Message from task: Hello
Message from task: 3.14
Message from task: 42

Como o algoritmo parallel_invoke executa tarefas simultaneamente, a ordem das mensagens de saída pode variar.

Para obter exemplos completos que mostram como usar o algoritmo parallel_invoke, consulte Como usar parallel_invoke para escrever uma rotina de classificação paralela e Como usar parallel_invoke para executar operações paralelas. Para obter um exemplo completo que usa a classe task_group para implementar futuros assíncronos, consulte Instruções passo a passo: implementando futuros.

Programação robusta

Certifique-se de entender a função de cancelamento e tratamento de exceções ao usar tarefas, grupos de tarefas e algoritmos paralelos. Por exemplo, em uma árvore de trabalho paralelo, o cancelamento de uma tarefa impede a execução das tarefas filho. Isso pode causar problemas quando uma das tarefas filho executa uma operação importante para o aplicativo, como a liberação de um recurso. Além disso, se uma tarefa filho gerar uma exceção, essa exceção poderá se propagar por meio de um destruidor de objeto e causar um comportamento indefinido em seu aplicativo. Para obter um exemplo que ilustra esses pontos, consulte a seção Entender como o cancelamento e o tratamento de exceções afetam a destruição de objetos nas práticas recomendadas no documento da Biblioteca de Padrões Paralelos. Para obter informações sobre os modelos de cancelamento e tratamento de exceções na PPL, confira Cancelamento e Tratamento de Exceção.

Título Descrição
Como usar parallel_invoke para escrever uma rotina de classificação em paralelo Mostra como usar o algoritmo parallel_invoke para melhorar o desempenho do algoritmo de classificação bitônica.
Como usar Parallel.Invoke para executar operações em paralelo Mostra como usar o algoritmo parallel_invoke para melhorar o desempenho de um programa que executa várias operações em uma fonte de dados compartilhada.
Como criar uma tarefa que seja concluída após um atraso Mostra como usar as classes task, cancellation_token_source, cancellation_token e task_completion_event para criar uma tarefa que é concluída após um atraso.
Instruções passo a passo: implementando futuros Mostra como combinar a funcionalidade existente no Runtime de Simultaneidade a algo que faz mais.
Biblioteca de padrões paralelos (PPL) Descreve o PPL, que fornece um modelo de programação imperativo para o desenvolvimento de aplicativos simultâneos.

Referência

Classe task (Runtime de Simultaneidade)

Classe task_completion_event

Função when_all

Função when_any

Classe task_group

Função parallel_invoke

Classe structured_task_group