Partilhar via


Paralelismo de Tarefas (Tempo de Execução Concorrente)

No Concurrency Runtime, 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 mais refinadas que são organizadas em um grupo de tarefas.

Você usa tarefas quando escreve 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 assincronamente de um arquivo e, em seguida, usar outra tarefa — uma tarefa de continuação, que é explicada mais adiante neste documento — para processar os dados depois que eles estiverem 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, em seguida, aguardar a conclusão do trabalho dividido.

Sugestão

Quando quiser aplicar a mesma rotina a todos os elementos de uma coleção em paralelo, use um algoritmo paralelo, como concurrency::p arallel_for, em vez de uma tarefa ou grupo de tarefas. Para obter mais informações sobre algoritmos paralelos, consulte Algoritmos paralelos.

Pontos Principais

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

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

  • Use grupos de tarefas (a classe concurrency::task_group ou o algoritmo concurrency::p arallel_invoke ) quando quiser decompor o trabalho paralelo em partes menores e, em seguida, aguarde 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 orientada por tarefas é sempre agendada para execução quando a tarefa antecedente é concluída, mesmo quando esta é cancelada ou lança uma exceção.

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

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

  • Para saber como o tempo de execução lida com exceções lançadas por tarefas e grupos de tarefas, consulte Tratamento de exceções.

Neste documento

Usando expressões do Lambda

Devido à sua sintaxe sucinta, as expressões lambda são uma maneira comum de definir o trabalho que é 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, esteja ciente 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 se captura por referência, não é feita uma cópia. Portanto, certifique-se de que o tempo de vida de qualquer variável capturada por referência sobreviva à 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 capturadas em expressões lambda, para que possa identificar o que está a capturar por valor em vez de 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ê essa variável. Não é possível capturar por valor porque cada tarefa de continuação conteria uma cópia diferente da variável. Para variáveis alocadas na 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 envolver a variável e passar o ponteiro inteligente por valor. Desta forma, o objeto subjacente pode ser atribuído e lido, e sobreviverá às tarefas que o utilizam. Use esta técnica mesmo quando a variável for um ponteiro ou um manipulador com contagem de referências (^) para um objeto do Tempo de Execução do Windows. Aqui está 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. Este modelo de composição é apoiado 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, todas as tarefas de continuação que estão aguardando nela são agendadas para 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 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 do início de uma tarefa ou de forma cooperativa enquanto está em execução. Para obter mais informações sobre este modelo de cancelamento, consulte Cancelamento no PPL.

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

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 task::wait método retorna um valor concorrência::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 quando você usa 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 concurrency::create_task , você pode usar a auto palavra-chave 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 create_task função 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 lançada durante a execução de uma tarefa, o runtime gere essa 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 de tarefas, consulte Tratamento de exceções.

Para obter um exemplo que usa task, concurrency::task_completion_event, cancelamento, consulte Guia Prático: Conexão usando tarefas e pedidos XML HTTP. (A task_completion_event classe é descrita mais adiante neste documento.)

Sugestão

Para saber detalhes específicos para 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 que uma operação assíncrona, após a conclusão, invoque uma segunda operação e passe dados para ela. Tradicionalmente, isso é feito usando funções de retorno. No Concurrency Runtime, a mesma funcionalidade é fornecida por tarefas de continuação. Uma tarefa de continuação (também conhecida apenas como continuação) é uma tarefa assíncrona que é invocada por outra tarefa, que é conhecida como antecedente, quando o antecedente é concluído. Usando continuações, você pode:

  • Passe os dados do antecedente para a continuação.

  • Especificar as condições precisas em que a continuação é invocada ou não invocada.

  • Pode cancelar uma continuação antes de começar ou cooperativamente enquanto estiver em execução.

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

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

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

  • Encadeie continuidades umas após as outras até qualquer extensão.

  • Utilize uma continuação para lidar com exceções lançadas pelo antecedente.

Esses recursos permitem que você execute uma ou mais tarefas quando a primeira tarefa for concluída. Por exemplo, você pode criar uma continuação que compacta um arquivo depois que a primeira tarefa o lê 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 antecedente quando ela estiver 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 encaixar tarefas em qualquer extensão. 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, esta tarefa é executada antes da continuação subsequente. Esta técnica é conhecida como desempacotamento assíncrono. O desempacotamento 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 desempacotamento da tarefa aninhada.

Value-Based versus Task-Based continuações

Dado um task objeto 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 assume o tipo T é conhecida como uma continuação baseada em valor. Uma continuação baseada em valor é agendada para execução quando a tarefa antecedente é concluída sem erro e não é cancelada. Uma continuação que toma o tipo task<T> como parâmetro é conhecida como uma continuação baseada em tarefas. Uma continuação orientada por tarefas é sempre agendada para execução quando a tarefa antecedente é concluída, mesmo quando esta é cancelada ou lança uma exceção. De seguida, podes chamar task::get para obter o resultado da tarefa antecedente. Se a tarefa antecedente for cancelada, task::get lança concurrency::task_canceled. Caso a tarefa antecedente tenha lançado uma exceção, task::get irá relançar essa exceção. Uma continuação baseada em tarefa não é marcada como cancelada quando sua tarefa antecedente é cancelada.

Tarefas de composição

Esta seção descreve as funções concorrência::when_all e concorrência::when_any, que podem ajudar a compor múltiplas tarefas para implementar padrões comuns.

A função when_all

A when_all função produz uma tarefa que é concluída após a conclusão de um conjunto de tarefas. Esta função retorna um objeto std::vetor 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 atribui a when_all devem ser uniformes. Em outras palavras, todos 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 a conclusão de um conjunto de tarefas. O exemplo a seguir modifica o anterior para imprimir a soma de três tarefas que cada uma produz um int resultado.

// 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 de um conjunto de tarefas for cancelada ou lançar uma exceção, when_all será concluída imediatamente e não aguardará a conclusão das tarefas restantes. Se uma exceção for lançada, o tempo de execução relançará a exceção ao chamar task::get ou task::wait no objeto de tarefa retornado por when_all. Se mais de uma tarefa for lançada, o tempo de execução escolhe uma delas. Portanto, certifique-se de observar todas as exceções após a conclusão de todas as tarefas; Uma exceção de tarefa não tratada faz com que o aplicativo seja encerrado.

Aqui está uma função de utilitário que você pode usar para garantir que seu programa observe todas as exceções. Para cada tarefa no intervalo fornecido, observe_all_exceptions aciona qualquer exceção ocorrida para ser relançada e, em seguida, a absorve.

// 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 no disco. O exemplo a seguir mostra como usar when_all e observe_all_exceptions 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 Button controle.
<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 da 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 Button_Click eventos.
// 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 como mostrado no exemplo.

Sugestão

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

A função when_any

A when_any função produz uma tarefa que é concluída quando a primeira tarefa de 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 when_any função é especialmente útil nos seguintes cenários:

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

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

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

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

Tal como acontece com when_all, é comum usar uma continuação que tenha when_any para executar uma ação quando a primeira de um conjunto de tarefas termina. O exemplo básico a seguir usa when_any para criar uma tarefa que é concluída quando a primeira das 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 no when_all, as tarefas que passa para when_any devem todas retornar do 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

Sugestão

Assim como o when_all, when_any não é bloqueante e é seguro chamá-lo numa aplicação 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 satisfeita, 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 utilizar uma destas técnicas. Por exemplo, para criar uma continuação, você deve ter a tarefa antecedente. No entanto, se você não tiver a tarefa antecedente, poderá criar um evento de conclusão de tarefa e, posteriormente, encadear esse evento de conclusão para a tarefa antecedente quando ela estiver disponível. Além disso, como uma tarefa em 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. Como a classe task, o parâmetro tipo T é o tipo do resultado que é produzido pela tarefa. Esse tipo pode ser void se a tarefa não retornar um valor. T não é possível usar o const modificador. Normalmente, um task_completion_event objeto é fornecido a um thread ou tarefa que o sinalizará quando o valor para ele estiver disponível. Ao mesmo tempo, uma ou mais tarefas são configuradas como ouvintes deste evento. Quando o evento é definido, as tarefas do ouvinte são concluídas e suas continuações são agendadas para serem executadas.

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

Grupos de Tarefas

Um grupo de tarefas organiza uma coleção de tarefas. Os grupos de tarefas enviam tarefas para uma fila de roubo de trabalho. O agendador remove tarefas dessa fila e as executa nos recursos de computação disponíveis. Depois de adicionar tarefas a um grupo de tarefas, você pode aguardar a conclusão de todas as tarefas ou cancelar 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 task_handle classe encapsula o código que executa o trabalho. Como a task classe, a função de trabalho assume a forma de uma função lambda, ponteiro de função ou objeto de função. Normalmente, não é necessário trabalhar diretamente com task_handle objetos. Em vez disso, você passa funções de trabalho para um grupo de tarefas e o grupo de tarefas cria e gerencia os task_handle objetos.

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

Importante

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

Use parallel_invoke quando você tiver várias tarefas independentes que deseja executar ao mesmo tempo e você deve esperar que todas as tarefas sejam concluídas antes de continuar. Esta técnica é muitas vezes referida como fork e join paralelismo. Use task_group quando tiver várias tarefas independentes que deseja executar ao mesmo tempo, mas quiser aguardar que as tarefas sejam concluídas posteriormente. Por exemplo, você pode adicionar tarefas a um task_group objeto e aguardar que as tarefas sejam concluídas em outra função ou em outro thread.

Os grupos de tarefas apoiam o conceito de cancelamento. O cancelamento permite que você sinalize para todas as tarefas ativas que deseja cancelar a operação geral. O cancelamento também impede que tarefas que ainda não começaram sejam iniciadas. Para obter mais informações sobre cancelamento, consulte Cancelamento no PPL.

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

Comparando task_group com structured_task_group

Embora recomendemos que você use task_group ou parallel_invoke em vez da structured_task_group classe, há casos em que você deseja usar structured_task_group, por exemplo, quando você escreve 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 task_group classes e structured_task_group .

A task_group classe é thread-safe. Portanto, você pode adicionar tarefas a um task_group objeto a partir de vários threads e aguardar ou cancelar um task_group objeto de vários threads. A construção e destruição de um structured_task_group objeto devem ocorrer no mesmo âmbito lexical. Além disso, todas as operações em um structured_task_group objeto devem ocorrer no mesmo thread. A exceção a esta regra é os métodos concurrency::structured_task_group::cancel e concurrency::structured_task_group::is_canceling. Uma tarefa filha pode chamar estes métodos para cancelar o grupo de tarefas principal ou verificar o cancelamento a qualquer momento.

Você pode executar tarefas adicionais em um task_group objeto 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 structured_task_group objeto 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 structured_task_group classe não sincroniza entre threads, ela tem menos sobrecarga de execução do que a task_group classe. Portanto, se o problema não exigir que você agende o trabalho de vários threads e não puder usar o parallel_invoke algoritmo, a structured_task_group classe pode ajudá-lo a escrever código com melhor desempenho.

Se você usar um structured_task_group objeto dentro de outro structured_task_group objeto, o objeto interno deverá terminar e ser destruído antes que o objeto externo termine. A task_group classe não requer que os grupos de tarefas aninhados terminem antes que o grupo externo termine.

Grupos de tarefas não estruturados e grupos de tarefas estruturados trabalham com manipuladores de tarefas de maneiras diferentes. Você pode passar funções de trabalho diretamente para um task_group objeto, o task_group objeto criará e gerenciará o identificador de tarefa para você. A structured_task_group classe requer que você gerencie um task_handle objeto para cada tarefa. Cada task_handle objeto deve permanecer válido durante toda a vida útil de seu objeto associado structured_task_group . Use a função concurrency::make_task para criar um task_handle objeto, 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 gerir identificadores de tarefas para casos em que se tem um número variável de tarefas, use uma rotina de alocação de pilha, como _malloca, ou uma classe de contentor, como std::vector.

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

Exemplo

O exemplo básico a seguir mostra como trabalhar com grupos de tarefas. Este exemplo usa o parallel_invoke algoritmo para executar duas tarefas simultaneamente. Cada tarefa adiciona subtarefas a um task_group objeto. Observe que a task_group classe 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();
}

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

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

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

Para obter exemplos completos que mostram como usar o parallel_invoke algoritmo, 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 para implementar futuros assíncronos task_group , consulte Passo a passo: Implementando futuros.

Programação robusta

Certifique-se de que compreende o papel do cancelamento e do tratamento de exceções quando utiliza tarefas, grupos de tarefas e algoritmos paralelos. Por exemplo, numa árvore de trabalho paralelo, uma tarefa cancelada impede a execução de tarefas subordinadas. Isso pode causar problemas se uma das tarefas subordinadas executar uma operação que é importante para a sua aplicação, como libertar um recurso. Além disso, se uma subtarefa lançar uma exceção, essa exceção poderá propagar-se por meio de um destruidor de objeto e causar um comportamento indefinido na sua aplicação. Para obter um exemplo que ilustra esses pontos, consulte a seção Compreender como o cancelamento e o tratamento de exceções afetam a destruição de objetos no documento Práticas recomendadas na Biblioteca de padrões paralelos. Para obter mais informações sobre os modelos de cancelamento e tratamento de exceções no PPL, consulte Cancelamento e tratamento de exceções.

Título Descrição
Como: Usar parallel_invoke para escrever uma rotina de classificação paralela Mostra como usar o parallel_invoke algoritmo para melhorar o desempenho do algoritmo de classificação bitônica.
Como: Usar parallel_invoke para executar operações paralelas Mostra como usar o parallel_invoke algoritmo para melhorar o desempenho de um programa que executa várias operações em uma fonte de dados compartilhada.
Como: Criar uma tarefa que é 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 se conclui após um atraso.
Passo a passo: Implementando futuros Mostra como combinar a funcionalidade existente no Concurrency Runtime em 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 de tarefa (Concurrency Runtime)

Classe Evento de Conclusão de Tarefa

Função when_all

Função when_any

task_group Classe

Função parallel_invoke

structured_task_group Classe