Compartilhar via


Paralelismo de tarefa (tempo de execução de simultaneidade)

No Tempo de Execução de Concorrência, uma tarefa é uma unidade de trabalho que executa um trabalho específico e geralmente é executada paralelamente a outras tarefas. Uma tarefa pode ser decomposta em tarefas adicionais mais refinadas que são organizadas em um grupo de trabalho.

Você usado tarefas quando escreve o código assíncrono e deseja que qualquer 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 um arquivo e usar outra tarefa — uma tarefa de continuação, que será explicada posteriormente neste documento — para processar os dados depois que ela se torna disponível. Inversamente, você pode usar grupos de tarefas para decompo 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 trabalho para executar simultaneamente esses partições, e esperar que o trabalho dividido se conclua.

Dica

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

Pontos-chave

  • Quando você passa as variáveis a uma expressão lambda por referência, você deve garantir que o tempo de vida da variável persiste até que a tarefa seja concluída.

  • Use tarefas (a classe concurrency::task) quando você escrever o código assíncrono.

  • Use grupos de trabalho (a classe concurrency::task_group ou o algoritmo concurrency::parallel_invoke) quando você deseja decompor o trabalho paralelo em partes menores e esperar as partes menores concluírem.

  • Use o método concurrency::task::then para criar continuações. Uma continuação é uma tarefa executada de forma assíncrona após outra tarefa terminar. 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 a execução quando a tarefa antecedente é concluída, mesmo quando a tarefa antecedente é cancelada ou gera uma exceção.

  • Use concurrency::when_all para criar uma tarefa que termine após a conclusão de todos os membros de um conjunto de tarefas. Use concurrency::when_any para criar uma tarefa que termine 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 no PPL.

  • Para saber como o tempo de execução trata exceções que são geradas por tarefas e por grupos de trabalho, consulte Tratamento de exceções no tempo de execução de simultaneidade.

Neste Documento

  • Usando Expressões Lambda

  • A Classe task

  • Tarefas de Continuação

  • Continuações com Base no Valor Contra com Base na Tarefa

  • Compondo Tarefas

    • A Função when_all

    • A Função when_any

  • Execução da Tarefa com Atraso

  • Grupos de Tarefas

  • Comparando task_group a structured_task_group

  • Exemplo

  • Programação robusta

Usando Expressões Lambda

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

  • Como as tarefas normalmente são executadas em segmentos de segundo plano, esteja ciente da vida útil do objeto ao capturar variáveis em expressões lambda. Quando você captura uma variável por valor, uma cópia da variável é feita no corpo lambda. Quando você capture por referência, uma cópia não é feita. Portanto, verifique se o tempo de vida útil de qualquer variável que você capture por referência dura mais do que a tarefa que ela usa.

  • Ao passar uma expressão lambda para uma tarefa, não capture variáveis que são atribuídas na pilha por referência.

  • Seja explícito sobre as variáveis que você captura nas expressões lambda para que seja possível identificar o que você está capturando por valor e 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 atribui a uma variável, e outra tarefa lê a variável. Você não pode capturar o valor porque cada tarefa de continuação de linha guardaria uma cópia diferente da variável. Para variáveis alocadas de pilha, você também não conseguirá capturar por referência porque a variável pode não ser mais válida.

Para resolver esse problema, use um ponteiro inteligente, como o std::shared_ptr, para envolver a variável e passar o ponteiro inteligente por valor. Dessa forma, o objeto subjacente pode ser atribuído e lido e vai durar mais do que as tarefas que o utilizam. Use essa técnica mesmo quando a variável for um ponteiro ou um manipulador contado por referência (^) para um objeto do Tempo de Execução do Windows. Eis 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 em C++.

[Superior]

A Classe task

Você pode usar a classe de concurrency::task para compor tarefas em um conjunto de operações dependentes. Não há suporte para este modelo de composição pela noção de continuações. Uma continuação permite que o código seja executado quando a tarefa anterior ou antecedente for concluída. O resultado da tarefa antecedente é passado como a entrada para uma ou mais tarefas de continuação. Quando uma tarefa antecedente é concluída, quaisquer tarefas de continuação de linha que esperarem nela são agendadas para a execução. Cada tarefa de continuação recebe uma cópia do resultado da tarefa antecedente. Por sua vez, essas tarefas de continuação também podem ser tarefas antecedentes para outras continuações, criando assim uma cadeia de tarefas. As continuações ajudam você a criar cadeias de de comprimento de tarefas arbitrário com dependências específicas entre elas. Além disso, uma tarefa pode participar de um cancelamento antes de a tarefa começar ou de uma forma cooperativa durante a execução. Para obter mais informações sobre esse modelo de cancelamento, consulte Cancelamento no PPL.

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

Quando você cria uma tarefa, você fornece uma função de trabalho que executa o corpo de tarefas. Essa função de trabalho vem na forma de uma função lambda, um ponteiro de função ou um objeto de função. Para esperar uma tarefa terminar sem obter o resultado, chame o método concurrency::task::wait. O método de task::wait retorna um valor de concurrency::task_status que descreve se a tarefa foi concluída ou cancelada. Para obter o resultado de tarefas, chame o método de concurrency::task::get. Este 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, esperar seu resultado e exibir seu valor. Os exemplos nesta documentação usam funções lambda, pois elas 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
*/

Quando você usa a função de concurrency::create_task , você pode usar a palavra-chave auto em vez de declarar o tipo. Por exemplo, considere este 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 é gerada durante a execução de uma tarefa, o tempo de execução efetua marshaling dessa exceção na chamada subsequente para task::get ou task::wait, ou para uma continuação baseada em tarefa. Para obter mais informações sobre o mecanismo de tratamento de exceções da tarefa, consulte Tratamento de exceções no tempo de execução de simultaneidade.

Para obter um exemplo que use o cancelamento de task, concurrency::task_completion_event, consulte Instruções passo a passo: conexão usando tarefas e solicitações HTTP XML. (A classe task_completion_event é descrita posteriormente neste documento.)

Dica

Para saber os detalhes que são específicos para tarefas em aplicativos Windows Store , consulte Asynchronous programming in C++ e Criando operações assíncronas n C++ para aplicativos da Windows Store.

[Superior]

Tarefas de Continuação

Na programação assíncrona, é muito comum para uma operação assíncrona, na conclusão, chamar uma segunda operação e passar dados para ela. Tradicionalmente, isso é feito usando métodos de retorno de chamada. No Tempo de Execução de Concorrência, a mesma funcionalidade é fornecida por tarefas de continuação. Uma tarefa de continuação (também conhecida apenas como uma continuação) é uma tarefa assíncrona que é chamada por outra tarefa, que é conhecida como antecedente, quando a tarefa antecedente é concluída. Usando continuações, você pode:

  • Passar dados do antecedente para a continuação.

  • Especificar as condições precisas sob as quais a continuação é 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 Windows Store. Para obter mais informações, consulte Criando operações assíncronas n C++ para aplicativos da Windows Store.)

  • Invoque várias continuações do mesmo antecedente.

  • Invoque uma continuação quando todos ou qualquer um dos vários antecedentes forem concluídos.

  • Continuações de encadeamento uma após a outra para qualquer comprimento.

  • Use uma continuação para manipular exceções geradas pelo antecedente.

Esses recursos permitem que você execute uma ou mais tarefas quando a primeira tarefa é concluída. Por exemplo, você pode criar uma continuação que compacte um arquivo depois que a primeira tarefa o ler no disco.

O exemplo a seguir modifica o exemplo anterior para usar o método concurrency::task::then para agendar uma continuação que mostra o valor da tarefa antecedente quando está 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 aninhar e encadear tarefas para qualquer comprimento. Uma tarefa também pode ter várias continuações. O exemplo a seguir ilustra uma cadeia básica de continuação 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, então esta tarefa será executada antes da continuação subsequente. Essa técnica é conhecida como desempacotar assíncrono. O desempacotamento assíncrono é útil quando você deseja realizar o trabalho adicional no plano de fundo, mas não deseja que a tarefa atual bloqueie o segmento atual. (Isso é comum em aplicativos de Windows Store, onde as continuações podem ser executadas no encadeamento de interface do usuário). O código 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 termina quando a tarefa aninhada completa.Em outras palavras, a continuação executa o desempacotamento da tarefa aninhada.

[Superior]

Continuações com Base no Valor Contra com Base na Tarefa

Dado um objeto task cujo tipo de retorno seja T, você pode fornecer um valor do tipo T ou task<T> a suas tarefas de continuação. Uma continuação que usa o tipo T é conhecida como uma continuação baseada em valor. Uma continuação baseada em valor é agendada para a execução quando a tarefa antecedente é concluída sem erro e não é cancelada. Uma continuação que usa o tipo task<T> como sendo seu parâmetro, também é conhecida como uma continuação baseada em tarefa. Uma continuação baseada em tarefa é sempre agendada para a execução quando a tarefa antecedente é concluída, mesmo quando a tarefa antecedente é cancelada ou gera uma exceção. Você pode chamar task::get para obter o resultado de tarefa antecedente. Se a tarefa antecedente tiver sido cancelada, task::get gerará concurrency::task_canceled. Se a tarefa antecedente gerou uma exceção, task::get gerará novamente a exceção. Uma continuação baseada em tarefa não é marcada como cancelada quando sua tarefa antecedente é cancelada.

[Superior]

Compondo Tarefas

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

A Função when_all

A função when_all gerencia uma tarefa que termina após um conjunto de tarefas concluídas. Esta 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.
*/

Dica

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 de && para gerar uma tarefa que termine após um conjunto de tarefas concluir, conforme mostrado no exemplo o 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 a conclusão de um conjunto de tarefas. O exemplo a seguir modifica o exemplo anterior para mostrar a soma de três tarefas em que cada uma produz 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.
*/

Nesse exemplo, você também pode especificar task<vector<int>> para gerar uma continuação baseada em tarefa.

Se qualquer tarefa em um conjunto de tarefas for cancelada ou se uma exceção for gerada, when_all será concluído imediatamente e não aguardará a conclusão das tarefas restantes. Se uma exceção for gerada, o tempo de execução gerará novamente a exceção quando você chamar task::get ou task::wait no objeto de tarefa que when_all retorna. Se mais de uma tarefa for gerada, o tempo de execução escolherá um deles. Portanto, certifique-se de observar 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 finalizado.

Eis uma função utilitária que você pode usar para garantir que seu programa observe todas as exceções. Para cada tarefa no intervalo fornecido, observe_all_exceptions dispara qualquer exceção que ocorra para ser relançada e então engole 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 do Windows Store 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 este exemplo

  1. Em MainPage.xaml, adicione um controle Button.

    <Button x:Name="Button1" Click="Button_Click">Write files</Button>
    
  2. Em MainPage.xaml.h, adicione essas declarações de avanço à 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);
    
  3. 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.
        });
    }
    
  4. Em MainPage.xaml.cpp, implemente WriteFilesAsync conforme mostrado no exemplo.

Dica

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

[Superior]

A Função when_any

A função when_any gerencia uma tarefa que termina quando a primeira tarefa em um conjunto de tarefas é concluída. Esta 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 seguintes cenários:

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

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

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

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

Tal como when_all, é comum usar uma continuação que tem when_any para executar a ação quando o primeiro em um conjunto de tarefas for concluído. O exemplo básico a seguir usa when_any para criar uma tarefa que é concluída quando a primeira das outras três 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.
*/

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

Dica

Tal como when_all, as tarefas que você passa para when_any devem todas retornar o mesmo tipo.

Você também pode usar a sintaxe de || para gerar uma tarefa que termine depois da primeira tarefa em um conjunto de tarefas concluídas, conforme mostrado no exemplo o seguir.

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

Dica

Tal como when_all, when_any não possui bloqueio e é seguro para chamar em um aplicativo do Windows Store no segmento de ASTA.

[Superior]

Execução da Tarefa com Atraso

Algumas vezes é necessário atrasar a execução de uma tarefa até que uma condição seja satisfeita, ou para iniciar uma tarefa em resposta a um evento externo. Por exemplo, na programação assíncrona, você pode ter de iniciar uma tarefa em resposta a um evento de conclusão de E/S.

Duas maneiras para fazer isso é usar uma continuação ou iniciar uma tarefa e esperar em um evento dentro da função de trabalho de tarefas. Entretanto, existem casos onde não é possível usar uma dessas técnicas. Por exemplo, para criar uma continuação, você deverá ter a tarefa antecedente. No entanto, se você não tiver a tarefa antecedente, poderá criar um evento de conclusão de tarefa e uma cadeia posterior que concluirá o evento para a tarefa antecedente quando ficar disponível. Além disso, como uma tarefa que esteja aguardando também bloqueia um thread, você poderá usar eventos de conclusão de tarefa para realizar trabalho quando uma operação assíncrona for concluída e, deste modo, libera um thread.

A classe concurrency::task_completion_event ajuda a simplificar tal composição de tarefas. Como a classe task, o parâmetro de tipo T é o tipo do resultado gerado pela tarefa. Esse tipo pode 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 a uma tarefa que o sinalizará quando o valor dele fica disponível. Ao mesmo tempo, uma ou mais tarefas são definidas como ouvintes do evento. Quando o evento é definido, as tarefas de ouvinte completam e suas continuações são agendadas para executar.

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

[Superior]

Grupos de Tarefas

Um grupo de trabalho organiza uma coleção de tarefas. Os grupos de tarefas enviam tarefas para uma fila de deslocamento de trabalho. O agendador remove as tarefas da fila e as executa nos recursos de computação disponíveis. Após adicionar tarefas a um grupo de trabalho, você pode esperar pela conclusão de todas as tarefas ou cancelar as tarefas que ainda não iniciaram.

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. Como a classe task, a função de trabalho vem na forma de uma função lambda, um ponteiro de função ou um objeto de função. Você normalmente não precisará trabalhar com objetos de task_handle diretamente. 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 trabalho nestas 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 paralelamente um conjunto de tarefas.Como o algoritmo de parallel_invoke tem uma sintaxe mais sucinta, recomendamos que você use-o ao invés da classe de structured_task_group quando possível.O tópico Algoritmos paralelos descreve parallel_invoke em detalhes.

Use parallel_invoke quando você tiver várias tarefas independentes que você deseja executar ao mesmo tempo, e você deve aguardar todas as tarefas terminar antes de continuar. Essa técnica é geralmente conhecida como paralelismo do tipo bifurcação e adição. Use task_group quando você tiver várias tarefas independentes que você deseja executar ao mesmo tempo, mas você deseja esperar as tarefas terminarem posteriormente. Por exemplo, você pode adicionar tarefas a um objeto task_group e aguardar que as tarefas sejam finalizadas 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 a todas as tarefas ativas que queira cancelar a operação geral. O cancelamento também impede que as tarefas que não foram iniciadas comecem. Para obter mais informações sobre cancelamento, consulte Cancelamento no PPL.

O tempo de execução também fornece um modelo de manipulação de exceção que permite lançar uma exceção de uma tarefa e tratar essa exceção quando você aguarda a conclusão do grupo de tarefas associado. Para obter mais informações sobre esse modelo de manipulação de exceção, consulte Tratamento de exceções no tempo de execução de simultaneidade.

[Superior]

Comparando task_group a structured_task_group

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

A classe task_group é segura para segmentos. 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 filha pode chamar esses métodos para cancelar o grupo de tarefas filhas ou verificar a existência de cancelamento a qualquer momento.

Você pode executar tarefas adicionais em um objeto de task_group depois de chamar o método de concurrency::task_group::wait ou de 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, então o comportamento é indefinido.

Como a classe de structured_task_group não sincroniza entre as threads, ela tem menos sobrecarga de execução do que a classe de task_group. Portanto, se o problema não requer que você agende o trabalho de vários threads e você não puder usar o algoritmo parallel_invoke, a classe structured_task_group poderá ajudá-lo a escrever um código de 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 conclua. A classe task_group não requer que os grupos de trabalho aninhados terminem antes que o grupo externo concluir.

Grupos de tarefa não estruturados e grupos de tarefas estruturados com alças de tarefa diferentes. Você pode passar funções de trabalho diretamente a um objeto de task_group ; o objeto de task_group criará e gerenciará o identificador de tarefa para você. A classe de structured_task_group requer que você gerencie um objeto de task_handle para cada tarefa. Todos os objetos task_handle devem permanecer válidos por toda a vida útil de seu 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 a manipulação de tarefas nos casos em que você tem um número de tarefas variável, use uma rotina de alocação em pilha como _malloca ou uma classe recipiente, como std::vector.

task_group e structured_task_group suportam cancelamento. Para obter mais informações sobre cancelamento, consulte Cancelamento no PPL.

[Superior]

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 de task_group permite várias tarefas para adicionar 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();
}

A seguir está a saída de exemplo para este exemplo:

  

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

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

[Superior]

Programação robusta

Certifique-se de que você entende 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 em paralelo, uma tarefa cancelada impede que tarefas filhas sejam executadas. Isso pode causar problemas se uma das tarefas filhas executa uma operação que é importante para seu aplicativo, como liberar um recurso. Além disso, se uma tarefa filho lançar uma exceção, a exceção poderia se propagar por um destrutor de objeto e causar comportamento indefinido em seu aplicativo. Para obter um exemplo que ilustre esses pontos, consulte a seção Entenda como o cancelamento e o tratamento de exceções afetam a destruição de objetos nas práticas recomendadas do documento Biblioteca de padrões em paralelo. Para obter mais informações sobre modelos de cancelamento e de tratamento de exceções no PPL, consulte Cancelamento no PPL e Tratamento de exceções no tempo de execução de simultaneidade.

[Superior]

Tópicos relacionados

Nome

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

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 termine após um atraso.

Instruções passo a passo: implementando futuros

Mostra como combinar as funcionalidades existentes no tempo de execução de simultaneidade em algo mais útil.

Biblioteca de padrões paralelos (PPL)

Descreve o PPL, que fornece um modelo imperativo de programação para desenvolver aplicativos simultâneos.

Referência

Classe task (Tempo de Execução 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