Share via


Paralelismo de tareas (Runtime de simultaneidad)

En el Runtime de simultaneidad, una tarea es una unidad de trabajo que realiza un trabajo específico y que se suele ejecutar en paralelo con otras tareas. Una tarea se puede descomponer en tareas adicionales más específicas que se organizan en un grupo de tareas.

Las tareas se usan al escribir código asincrónico y cuando se quiere que alguna operación se produzca después de que finalice la operación asincrónica. Así, por ejemplo, se podría usar una tarea para leer un archivo de forma asincrónica y, luego, usar otra tarea —una tarea de continuación, descrita más adelante en este documento— para procesar los datos cuando estén disponibles. A la inversa, se pueden usar grupos de tareas para descomponer el trabajo paralelo en partes más pequeñas. Supongamos, por ejemplo, que tiene un algoritmo recursivo que divide el trabajo restante en dos partes. Puede usar grupos de tareas para ejecutar estas partes simultáneamente y esperar a que el trabajo dividido se complete.

Sugerencia

Para emplear la misma rutina en cada elemento de una colección en paralelo, use un algoritmo paralelo, como concurrency::parallel_for, en lugar de una tarea o un grupo de tareas. Para obtener más información sobre los algoritmos en paralelo, vea Algoritmos paralelos.

Puntos clave

  • Cuando se pasan variables por referencia a una expresión lambda, es necesario garantizar que esas variables van a persistir hasta que la tarea finalice.

  • Use tareas (la clase concurrency::task) al escribir código asincrónico. La clase task usa como programador el grupo de subprocesos de Windows, no el Runtime de simultaneidad.

  • Use grupos de tareas (la clase concurrency::task_group o el algoritmo concurrency::parallel_invoke) cuando quiera descomponer el trabajo paralelo en partes más pequeñas y esperar a que las partes más pequeñas se completen.

  • Use el método concurrency::task::then para crear continuaciones. Una continuación es una tarea que se ejecuta de forma asincrónica una vez completada otra tarea. Puede conectar un número indeterminado de continuaciones para formar una cadena de trabajo asincrónico.

  • Una continuación basada en tareas está programada siempre para ejecutarse cuando finaliza la tarea antecedente, aun cuando esta se cancele o genere una excepción.

  • Use concurrency::when_all para crear una tarea que se complete después de que cada miembro de un conjunto de tareas finalice. Use concurrency::when_any para crear una tarea que se complete después de que uno de los miembros de un conjunto de tareas finalice.

  • Las tareas y los grupos de tareas pueden participar en el mecanismo de cancelación de la Biblioteca de patrones de procesamiento paralelo (PPL). Para más información, consulte Cancelación en la biblioteca PPL.

  • Para saber el modo en que el runtime controla las excepciones generadas por las tareas y los grupos de tareas, consulte Control de excepciones.

En este documento

Uso de expresiones lambda

Dada su sintaxis concisa, las expresiones lambda son una forma habitual de definir el trabajo que las tareas y los grupos de tareas llevan a cabo. Estas son algunas sugerencias de uso:

  • Como las tareas normalmente se ejecutan en subprocesos en segundo plano, tenga en cuenta la duración del objeto cuando capture variables en expresiones lambda. Cuando se captura una variable por valor, se hace una copia de esa variable en el cuerpo de la lambda. Esta copia no se realiza cuando se captura por referencia. Por lo tanto, asegúrese de que la duración de la variable que capture por referencia es mayor que la tarea que la usa.

  • Al pasar una expresión lambda a una tarea, no capture variables que estén asignadas en la pila por referencia.

  • Sea explícito con respecto a las variables que capture en las expresiones lambda; así, podrá identificar lo que está capturando por valor en contraposición a por referencia. Por este motivo, recomendamos no usar las opciones [=] o [&] en expresiones lambda.

Un patrón común tiene lugar cuando una tarea en una cadena de continuación se asigna a una variable y otra tarea lee esa variable. En este caso no se podría capturar por valor, porque cada tarea de continuación contendría una copia diferente de la variable. En el caso de las variables asignadas a una pila, tampoco se podrá capturar por referencia, dado que es posible que la variable ya no sea válida.

Para solucionar este problema, use un puntero inteligente, como std::shared_ptr, para ajustar la variable y pasar el puntero inteligente por valor. De este modo, el objeto subyacente se puede asignar y leer, y su duración será mayor que la de las tareas que lo usan. Use esta técnica incluso cuando la variable sea un puntero o un identificador de recuento de referencia (^) a un objeto de Windows Runtime. Este es un ejemplo 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 obtener más información sobre las expresiones lambda, vea Expresiones lambda.

La clase task

Puede usar la clase concurrency::task para crear tareas dentro de un conjunto de operaciones dependientes. Este modelo de composición se sustenta en el concepto de continuaciones. Una continuación permite ejecutar código cuando la tarea anterior, o antecedente, se completa. El resultado de la tarea antecedente se pasa como entrada para una o varias tareas de continuación. Cuando una tarea antecedente se completa, las tareas de continuación en espera están programadas para ejecutarse. Cada tarea de continuación recibe una copia del resultado de la tarea anterior. Las tareas de continuación también pueden ser, a su vez, tareas antecedentes de otras continuaciones, lo que hace que se vaya creando una cadena de tareas. Las continuaciones sirven para crear cadenas de tareas de longitud arbitraria que tienen dependencias específicas entre ellas. Asimismo, una tarea puede participar en la cancelación antes de que una tarea se inicie o bien de manera cooperativa, mientras se está ejecutando. Para más información sobre el modelo de cancelación, consulte Cancelación en la biblioteca PPL.

task es una clase de plantilla. El parámetro de tipo T es el tipo del resultado generado por la tarea. Este tipo puede ser void si la tarea no devuelve un valor. T no puede usar el modificador const.

Cuando se crea una tarea, hay que proporcionar una función de trabajo que realice el cuerpo de la tarea. Esta función de trabajo tiene la forma de una función lambda, un puntero de función o un objeto de función. Para esperar a que una tarea finalice sin obtener el resultado correspondiente, llame al método concurrency::task::wait. El método task::wait devuelve un valor concurrency::task_status que indica si la tarea se completó o canceló. Para obtener el resultado de la tarea, llame al método concurrency::task::get. Este método llama a task::wait para esperar a que la tarea finalice y, por lo tanto, bloquea la ejecución del subproceso actual hasta que el resultado esté disponible.

En el siguiente ejemplo se muestra cómo crear una tarea, esperar su resultado y mostrar el valor correspondiente. En los ejemplos de esta documentación se usan funciones lambda porque proporcionan una sintaxis más concisa. Sin embargo, también se pueden usar objetos de función y punteros de función al trabajar con tareas.

// 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
*/

Cuando se usa la función concurrency::create_task, puede usar la palabra clave auto en lugar de declarar el tipo. Veamos, por ejemplo, este código con el que se crea e imprime la matriz de identidad:

// 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
*/

Puede usar la función create_task para crear la operación 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;
});

Si se produce una excepción mientras una tarea se ejecuta, el Runtime calcula las referencias a esa excepción en la siguiente llamada a task::get o task::wait, o bien a una continuación basada en tareas. Para más información sobre el mecanismo de control de excepciones de tareas, consulte Control de excepciones.

Para ver un ejemplo en el que se usa task, concurrency::task_completion_event y cancelación, consulte Tutorial: Conectar con tareas y solicitudes HTTP XML. (La clase task_completion_event se describe más adelante en este documento).

Sugerencia

Para ver detalles concretos de tareas en aplicaciones para UWP, consulte Programación asincrónica en C# y Creación de operaciones asincrónicas en C++ para aplicaciones para UWP.

Tareas de continuación

En la programación asincrónica, es muy común que una operación asincrónica, al finalizar, invoque una segunda operación y le pase los datos. Tradicionalmente, esto se realiza mediante métodos de devolución de llamada. En el Runtime de simultaneidad, esta misma funcionalidad se proporciona por medio de tareas de continuación. Una tarea de continuación (también conocida simplemente como una continuación) es una tarea asincrónica invocada por otra tarea, conocida como el antecedente, cuando esta finaliza. El uso de continuaciones permite hacer lo siguiente:

  • Pasar datos del antecedente a la continuación.

  • Especificar las condiciones exactas en las que se invoca o no se invoca la continuación.

  • Cancelar una continuación antes de que se inicie o de forma cooperativa mientras se ejecuta.

  • Proporcionar sugerencias sobre cómo debería programarse la continuación. (Esto se aplica solo a las aplicaciones para la Plataforma universal de Windows (UWP). Para más información, consulte Creación de operaciones asincrónicas en C++ para aplicaciones UWP.

  • Invocar varias continuaciones desde el mismo antecedente.

  • Invocar una continuación cuando todas o una de las tareas antecedentes finalicen.

  • Encadenar continuaciones una tras otra de cualquier longitud.

  • Usar una continuación para controlar las excepciones producidas por el antecedente.

Estas características permiten ejecutar una o más tareas cuando la primera tarea se completa. Por ejemplo, puede crear una continuación que comprenda un archivo después de que la primera tarea lo lea desde el disco.

En el siguiente ejemplo se modifica el ejemplo anterior con el propósito de usar el método concurrency::task::then para programar una continuación que imprime el valor de la tarea anterior cuando esté disponible.

// 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
*/

Puede encadenar y anidar tareas de cualquier longitud. Una tarea también puede tener varias continuaciones. En el siguiente ejemplo se muestra una cadena de continuación básica que triplica el valor de la tarea anterior.

// 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
*/

Una continuación también puede devolver otra tarea. Si no hay ninguna cancelación, esta tarea se ejecuta antes de la continuación siguiente. Esta técnica se conoce como desencapsulación asincrónica. La desencapsulación asincrónica resulta útil cuando se quiere realizar un trabajo adicional en segundo plano sin que la tarea actual bloquee el subproceso actual. (Esto es habitual en aplicaciones para UWP, donde las continuaciones se pueden ejecutar en el subproceso de interfaz de usuario). En el siguiente ejemplo se muestran tres tareas. La primera tarea devuelve otra tarea que se ejecuta antes de una tarea de continuación.

// 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

Cuando una continuación de una tarea devuelve una tarea anidada de tipo N, la tarea resultante tiene el tipo N (no task<N>) y se completa cuando lo haga la tarea anidada. En otras palabras, la continuación realiza la desencapsulación de la tarea anidada.

Continuaciones basadas en valores frente a continuaciones basadas en tareas

Si tenemos un objeto task cuyo tipo de valor devuelto es T, se puede proporcionar un valor de tipo T o task<T> a las tareas de continuación. Una continuación que toma el tipo T se conoce como continuación basada en valores. Una continuación basada en valores está programada para ejecutarse cuando la tarea antecedente se completa sin errores y no se ha cancelado. Una continuación que toma el tipo task<T> como parámetro se conoce como continuación basada en tareas. Una continuación basada en tareas está programada siempre para ejecutarse cuando finaliza la tarea antecedente, aun cuando esta se cancele o genere una excepción. Tras ello, puede llamar a task::get para obtener el resultado de la tarea antecedente. Si la tarea antecedente se canceló, task::get genera concurrency::task_canceled. Si la tarea antecedente produjo una excepción, task::get vuelve a producir esta excepción. Una continuación basada en tareas no se marca como cancelada cuando su tarea antecedente se cancela.

Componer tareas

En esta sección se describen las funciones concurrency::when_all y concurrency::when_any, que hacen que sea más fácil crear varias tareas para implementar patrones comunes.

Función when_all

La función when_all genera una tarea que finaliza después de que se complete un conjunto de tareas. Esta función devuelve un objeto std::vector que contiene el resultado de cada tarea del conjunto. En el siguiente ejemplo básico se usa when_all para crear una tarea que representa la finalización de otras tres tareas.

// 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.
*/

Nota:

Las tareas que se pasan a when_all tienen que ser uniformes. En otras palabras, todas tienen que devolver el mismo tipo.

También puede usar la sintaxis && para crear una tarea que finalice cuando lo haga un conjunto de tareas, como se aprecia en el siguiente ejemplo.

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

Es habitual usar una continuación junto con when_all para realizar una acción después de que un conjunto de tareas finalice. En el siguiente ejemplo se modifica el ejemplo anterior con el propósito de imprimir la suma de tres tareas que generan un resultado int cada una.

// 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.
*/

En este ejemplo, también puede especificar task<vector<int>> para generar una continuación basada en tareas.

Si una tarea de un conjunto de tareas se cancela o genera una excepción, when_all se completa inmediatamente y no se espera a que las tareas restantes terminen. Si se produce una excepción, el Runtime volverá a producirla cuando se llame a task::get o a task::wait en el objeto de tarea que when_all devuelve. Si se produce una excepción en más de una tarea, el Runtime elige una de ellas. Por lo tanto, procure observar todas las excepciones después de que todas las tareas se hayan completado; una excepción de tarea no controlada hará que la aplicación finalice.

A continuación mostramos una función de utilidad que le servirá para asegurarse de que su programa tenga presentes todas las excepciones. Por cada tarea en el intervalo proporcionado, observe_all_exceptions desencadena cualquier excepción que se haya vuelto a producir y, luego, la pasa.

// 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.
            }
        });
    });
}

Imaginemos una aplicación para UWP que usa C++ y XAML y escribe un conjunto de archivos en el disco. En el siguiente ejemplo se muestra cómo usar when_all y observe_all_exceptions para asegurarse de que el programa tiene presentes todas las excepciones.

// 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 ejecutar este ejemplo
  1. En MainPage.xaml, agregue un control Button.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. En MainPage.xaml.h, agregue estas declaraciones adelantadas a la sección private de la declaración de clase 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. En MainPage.xaml.cpp, implemente el controlador 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. En MainPage.xaml.cpp, implemente WriteFilesAsync tal y como se muestra en el ejemplo.

Sugerencia

when_all es una función sin bloqueo que genera task como resultado. A diferencia de task::wait, resulta seguro llamar a esta función en una aplicación para UWP en el subproceso ASTA (STA de aplicación).

Función when_any

La función when_any genera una tarea que finaliza después de que lo haga la primera tarea de un conjunto de tareas. Esta función devuelve un objeto std::pair que contiene el resultado de la tarea completada y el índice de la tarea en el conjunto.

La función when_any es especialmente útil en los siguientes escenarios:

  • Operaciones redundantes. Considere un algoritmo o una operación que pueda realizarse de muchas maneras. Puede usar la función when_any para seleccionar la operación que finaliza primero y, luego, cancelar las operaciones restantes.

  • Operaciones intercaladas. Puede iniciar varias operaciones que tienen que finalizar en su totalidad y usar la función when_any para procesar los resultados cuando cada operación finalice. Finalizada una operación, puede iniciar una o más tareas adicionales.

  • Operaciones limitadas. Puede usar la función when_any para ampliar el escenario anterior limitando el número de operaciones simultáneas.

  • Operaciones que han expirado. Puede usar la función when_any para seleccionar entre una o más tareas y una tarea que finaliza después de una hora específica.

Al igual que when_all, es habitual usar una continuación que tiene when_any para realizar una acción al finalizar la primera tarea de un conjunto de tareas. En el siguiente ejemplo básico se usa when_any para crear una tarea que se completa cuando lo hace la primera de otras tres tareas.

// 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.
*/

En este ejemplo, también puede especificar task<pair<int, size_t>> para generar una continuación basada en tareas.

Nota:

Al igual que when_all, todas las tareas que se pasan a when_any tienen que devolver el mismo tipo.

También puede usar la sintaxis || para crear una tarea que finalice cuando lo haga la primera tarea de un conjunto de tareas, como se aprecia en el siguiente ejemplo.

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

Sugerencia

Al igual que when_all, when_any no aplica bloqueos y se puede llamar de forma segura en una aplicación para UWP en el subproceso ASTA.

Ejecución de tareas retrasadas

Hay veces que es necesario retrasar la ejecución de una tarea hasta que se satisfaga una condición, o iniciar una tarea en respuesta a un evento externo. Por ejemplo, en una programación asincrónica, puede que tenga que iniciar una tarea en respuesta a un evento de finalización de E/S.

Dos formas de lograrlo son usar una continuación o bien iniciar una tarea y esperar un evento dentro de la función de trabajo de la tarea. Sin embargo, hay casos en los que no es posible recurrir a una de estas técnicas. Por ejemplo, para crear una continuación, hay que tener la tarea antecedente. Pero si no tiene la tarea antecedente, puede crear un evento de finalización de tarea y, más adelante, encadenar ese evento de finalización a la tarea antecedente cuando esta esté disponible. Además, como una tarea en espera también bloquea un subproceso, puede usar eventos de finalización de tarea para realizar el trabajo cuando una operación asincrónica se complete y, de este modo, liberar un subproceso.

La clase concurrency::task_completion_event hace que sea más sencillo crear tareas. Al igual que la clase task, el parámetro de tipo T es el tipo del resultado que la tarea genera. Este tipo puede ser void si la tarea no devuelve un valor. T no puede usar el modificador const. Normalmente, se proporciona un objeto task_completion_event a un subproceso o una tarea que indicará el momento en el que el valor esté disponible. Al mismo tiempo, se establecen una o varias tareas como agentes de escucha de ese evento. Cuando se establece el evento, las tareas de agente de escucha finalizan y sus continuaciones se programan para ejecutarse.

Para ver un ejemplo en el que se usa task_completion_event para implementar una tarea que se completa después de un retraso, consulte Cómo crear una tarea que se complete después de un retraso.

Grupos de tareas

Un grupo de tareas organiza una colección de tareas. Los grupos de tareas envían tareas a una cola de robo de trabajo. El programador quita las tareas de esta cola y las ejecuta en los recursos informáticos disponibles. Después de agregar tareas a un grupo de tareas, puede esperar a que todas las tareas finalicen o cancelar las tareas que aún no se han iniciado.

La biblioteca PPL usa las clases concurrency::task_group y concurrency::structured_task_group para representar grupos de tareas y la clase concurrency::task_handle para representar las tareas que se ejecutan en estos grupos. La clase task_handle encapsula el código que realiza el trabajo. Al igual que la clase task, la función de trabajo tiene la forma de una función lambda, un puntero de función o un objeto de función. Normalmente no es necesario trabajar con objetos task_handle directamente. En su lugar, se pasan funciones de trabajo a un grupo de tareas y el grupo de tareas crea y administra los objetos task_handle.

La biblioteca PPL divide los grupos de tareas en estas dos categorías: grupos de tareas sin estructura y grupos de tareas con estructura. La biblioteca PPL usa la clase task_group para representar grupos de tareas sin estructura y la clase structured_task_group para representar grupos de tareas con estructura.

Importante

La PPL también define el algoritmo concurrency::parallel_invoke, que usa la clase structured_task_group para ejecutar un conjunto de tareas en paralelo. Como el algoritmo parallel_invoke tiene una sintaxis más concisa, recomendamos usarlo en lugar de la clase structured_task_group siempre que sea posible. En el tema Algoritmos paralelos se explica parallel_invoke en mayor profundidad.

Use parallel_invoke cuando tenga varias tareas independientes que quiere ejecutar al mismo tiempo y tenga que esperar a que todas las tareas finalicen antes de continuar. Esta técnica se conoce a menudo como paralelismo de bifurcación y unión. Use task_group cuando tenga varias tareas independientes que quiere ejecutar al mismo tiempo, pero quiera esperar a que las tareas finalicen más adelante. Por ejemplo, puede agregar tareas a un objeto task_group y esperar a que las tareas finalicen en otra función o desde otro subproceso.

Los grupos de tareas admiten el concepto de cancelación. Con la cancelación puede indicar a todas las tareas activas que quiere cancelar la operación global. De igual modo, las cancelaciones evitan que se inicien las tareas que aún no han empezado. Para obtener más información sobre la cancelación, vea Cancelación en la biblioteca PPL.

El Runtime también proporciona un modelo de control de excepciones que permite generar una excepción desde una tarea y controlar esa excepción mientras espera a que el grupo de tareas asociado finalice. Para más información sobre este modelo de control de excepciones, consulte Control de excepciones.

Comparación de task_group con structured_task_group

Aunque recomendamos usar task_group o parallel_invoke en lugar de la clase structured_task_group, hay casos donde probablemente prefiera usar structured_task_group, por ejemplo, al escribir un algoritmo paralelo que realiza un número variable de tareas o que requiere compatibilidad con la cancelación. En esta sección se explican las diferencias entre las clases task_group y structured_task_group.

La clase task_group es segura para la ejecución de subprocesos. Por lo tanto, puede agregar tareas a un objeto task_group desde varios subprocesos y esperar o cancelar un objeto task_group desde varios subprocesos. Es necesario que la construcción y destrucción de un objeto structured_task_group ocurran en el mismo ámbito léxico. Además, todas las operaciones en un objeto structured_task_group tienen que producirse en el mismo subproceso. La excepción a esta regla son los métodos concurrency::structured_task_group::cancel y concurrency::structured_task_group::is_canceling. Una tarea secundaria puede llamar a estos métodos para cancelar el grupo de tareas primario y buscar la cancelación en cualquier momento.

Puede ejecutar más tareas en un objeto task_group después de llamar a los métodos concurrency::task_group::wait o concurrency::task_group::run_and_wait. Por el contrario, si ejecuta más tareas en un objeto structured_task_group después de llamar a los métodos concurrency::structured_task_group::wait o concurrency::structured_task_group::run_and_wait, el comportamiento será indefinido.

La clase structured_task_group no se sincroniza entre subprocesos, de modo que tiene menos sobrecarga de ejecución que la clase task_group. Por lo tanto, si su problema no requiere programar el trabajo desde varios subprocesos y no puede usar el algoritmo parallel_invoke, la clase structured_task_group le ayudará a escribir código con mejor rendimiento.

Si usa un objeto structured_task_group dentro de otro objeto structured_task_group, el objeto interno debe finalizar y destruirse antes de que el objeto externo finalice. La clase task_group no requiere que los grupos de tareas anidadas finalicen antes de que finalice el grupo externo.

Los grupos de tareas con y sin estructura funcionan con identificadores de tareas de distinta forma. Se pueden pasar funciones de trabajo directamente a un objeto task_group; el objeto task_group creará y administrará el identificador de tarea automáticamente. La clase structured_task_group requiere que se administre un objeto task_handle por cada tarea. Cada objeto task_handle tiene que ser válido a lo largo de toda la duración del objeto structured_task_group asociado. Use la función concurrency::make_task para crear un objeto task_handle, como se indica en el siguiente ejemplo básico:

// 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 administrar los identificadores de tarea en situaciones en las que hay un número variable de tareas, use una rutina de asignación de pila como _malloca o una clase de contenedor, como std::vector.

Tanto task_group como structured_task_group admiten la cancelación. Para obtener más información sobre la cancelación, vea Cancelación en la biblioteca PPL.

Ejemplo

En el siguiente ejemplo básico se indica cómo trabajar con grupos de tareas. En él se usa el algoritmo parallel_invoke para realizar dos tareas simultáneamente. Cada tarea agrega subtareas a un objeto task_group. Tenga en cuenta que la clase task_group permite que varias tareas agreguen tareas al objeto al mismo tiempo.

// 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();
}

Aquí mostramos la salida de muestra de este ejemplo:

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

El algoritmo parallel_invoke ejecuta tareas de forma simultánea, por lo que el orden de los mensajes de salida podría variar.

Para ver ejemplos completos que reflejen cómo usar el algoritmo parallel_invoke, consulte Cómo usar parallel.invoke para escribir una rutina de ordenación en paralelo y Cómo usar parallel.invoke para ejecutar operaciones paralelas. Para ver un ejemplo completo donde se usa la clase task_group para implementar futuros asincrónicos, consulte Tutorial: Implementar futuros.

Programación sólida

Asegúrese de que comprende el rol de cancelación y control de excepciones cuando use tareas, grupos de tareas y algoritmos paralelos. Por ejemplo, en un árbol de trabajo paralelo, una tarea que se cancela impide que se ejecuten las tareas secundarias. Esto puede causar problemas si una de las tareas secundarias realiza una operación que tiene importancia para la aplicación, como liberar un recurso. Además, si una tarea secundaria genera una excepción, esta podría propagarse a través de un destructor de objetos y provocar un comportamiento no definido en la aplicación. Para ver un ejemplo que ilustre estos aspectos, consulte la sección Comprender cómo afectan la cancelación y el control de excepciones a la destrucción de objetos del tema de procedimientos recomendados de la biblioteca de patrones de procesamiento paralelo. Para más información sobre los modelos de cancelación y control de excepciones de la biblioteca PPL, consulte Cancelación y Control de excepciones.

Title Descripción
Procedimiento para usar parallel.invoke para escribir una rutina de ordenación en paralelo Muestra cómo usar el algoritmo parallel_invoke para mejorar el rendimiento del algoritmo de ordenación bitónica.
Procedimiento para usar parallel.invoke para ejecutar operaciones paralelas Muestra cómo usar el algoritmo parallel_invoke para mejorar el rendimiento de un programa que realiza varias operaciones en un origen de datos compartido.
Procedimiento para crear una tarea que se complete después de un retraso Explica cómo usar las clases task, cancellation_token_source, cancellation_token y task_completion_event para crear una tarea que se completa después de un retraso.
Tutorial: Implementar futuros Muestra cómo combinar la funcionalidad existente en el Runtime de simultaneidad para ampliar capacidades.
Biblioteca de modelos de procesamiento paralelo (PPL) Describe la biblioteca PPL, que proporciona un modelo de programación imperativo para desarrollar aplicaciones simultáneas.

Referencia

task (clase) (Runtime de simultaneidad)

task_completion_event (clase)

función when_all

función when_any

task_group (clase)

función parallel_invoke

structured_task_group (clase)