Compartir a través de


Paralelismo de tareas (Runtime de simultaneidad)

En el runtime de simultaneidad, una tarea es una unidad de trabajo que realiza un trabajo concreto y se ejecuta normalmente en paralelo con otras tareas. Una tarea se puede descomponer en tareas adicionales, más específicas, organizadas en un grupo de tareas.

Use tareas si escribe código asincrónico y desea que se produzca alguna operación cuando se complete la operación asincrónica. Por ejemplo, puede utilizar una tarea para leer asincrónicamente de un archivo y después utilizar otra tarea (una tarea de continuación, que se explica más adelante en este documento) para procesar los datos después de que estén disponibles. Por el contrario, puede 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 particiones. Puede usar grupos de tareas para ejecutar estas particiones simultáneamente y después esperar a que el trabajo dividido se complete.

SugerenciaSugerencia

Cuando desee aplicar la misma rutina a todos los elementos de una colección en paralelo, use un algoritmo paralelo tal como concurrency::parallel_for, en lugar de una tarea o un grupo de tareas.Para obtener más información acerca de los algoritmos paralelos, vea Algoritmos paralelos.

Puntos clave

  • Al pasar las variables a una expresión lambda por referencia, se debe garantizar que la duración de esa variable persista hasta que finalice la tarea.

  • Use las tareas (la clase concurrency::task) cuando escriba código asincrónico.

  • Use los grupos de tareas (la clase concurrency::task_group o el algoritmo concurrency::parallel_invoke) cuando desee descomponer el trabajo en paralelo en partes más pequeñas y después esperar a que se completen esos fragmentos pequeños.

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

  • Una continuación basada e tareas se programa siempre para la ejecución cuando finaliza la tarea antecedente, incluso cuando se cancela la tarea antecedente o se produce una excepción.

  • Utilice concurrency::when_all para crear una tarea que se complete después de que se complete cada miembro de un conjunto de tareas. Utilice concurrency::when_any para crear una tarea que se complete después de que se complete un miembro de un conjunto de tareas.

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

  • Para saber cómo controla el runtime las excepciones que producen las tareas y los grupos de tareas vea Control de excepciones en el runtime de simultaneidad.

En este documento

  • Utilizar expresiones lambda

  • La clase task

  • Tareas de continuación

  • Continuaciones basadas en valores frente a continuaciones basadas en tareas

  • Componer tareas

    • La función when_all

    • La función when_any

  • Ejecución retrasada de una tarea

  • Grupos de tareas

  • Comparación entre task_group y structured_task_group

  • Ejemplo

  • Programación sólida

Utilizar expresiones lambda

Debido a su sintaxis concisa, las expresiones lambda son una manera común para definir el trabajo realizado por tareas y grupos de tareas. A continuación se muestran algunas sugerencias de uso:

  • Dado que las tareas se ejecutan normalmente en subprocesos de segundo plano, tenga en cuenta la duración del objeto cuando capture variables en expresiones lambda. Cuando se captura una variable por valor, una copia de esa variable se hace en el cuerpo de una expresión lambda. Cuando se captura por referencia, no se realiza una copia. Por consiguiente, asegúrese de que la duración de una variable que se captura por referencia sobrevive a la tarea que la utiliza.

  • Cuando se pasa una expresión lambda a una tarea, no se capturan las variables que están asignadas en la pila por referencia.

  • Sea explícito sobre las variables que capture en expresiones lambda, para que pueda identificar qué está capturando por valor o por referencia. Por eso se recomienda no utilizar las opciones [=] o [&] para las expresiones lambda.

Un patrón común es cuando una tarea de una cadena de continuación se asigna a una variable y otra tarea lee esa variable. No puede capturar por valor porque cada tarea de continuación llevaría a contener otra copia de la variable. Para variables asignadas a la pila, tampoco puede capturar por referencia ya que la variable puede no ser 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 esta manera, el objeto subyacente se puede asignar a, leer en y sobrevivirá a las tareas que lo usan. Utilice esta técnica incluso cuando la variable sea un puntero o un controlador de contador de referencias(^) para un objeto de Windows en tiempo de ejecución. A continuación se muestra 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 en C++.

[Arriba]

La clase task

Puede usar la clase concurrency::task para redactar tareas en un conjunto de operaciones dependientes. Este modelo de composición es compatible con la noción de continuaciones. Una continuación habilita código que se ejecutará cuando se complete la tarea anterior o antecedente. El resultado de la tarea anterior se pasa como la entrada a una o más tareas de continuación. Cuando una tarea anterior se complete, cualquier tarea de continuación que está esperando por ella se programa para la ejecución. Cada tarea de continuación recibe una copia del resultado de la tarea anterior. A su vez, esas tareas de continuación también pueden ser tareas antecedentes para otras continuaciones, creando de ese modo una cadena de tareas. Las continuaciones ayudan a crear cadenas de la longitud arbitraria de tareas que tienen dependencias concretas entre ellas. Además, una tarea puede participar en la cancelación, o bien antes de que se inicien las tareas o de manera cooperativa mientras se ejecuta. Para obtener más información sobre este modelo de cancelación, vea Cancelación en la biblioteca PPL.

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

Cuando se crea una tarea, se proporciona una función de trabajo que realiza el cuerpo de la tarea. Esta función de trabajo tiene forma de una función lambda, puntero a función u objeto de función. Para esperar a que una tarea finalice sin obtener el resultado, llame al método concurrency::task::wait. El método task::wait devuelve un valor concurrency::task_status que describe si la tarea se ha completado o cancelado. Para obtener el resultado de la tarea, llame al método concurrency::task::get. Este método llama a task::wait para esperar hasta que finaliza la tarea y, por consiguiente, bloquea la ejecución del subproceso actual hasta que el resultado esté disponible.

El ejemplo siguiente muestra cómo crear una tarea, esperar el resultado y mostrar su valor. Los ejemplos de esta documentación usan funciones lambda porque proporcionan una sintaxis más concisa. Sin embargo, también puede utilizar punteros a función y objetos de función cuando usa 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 utilice la función concurrency::create_task, puede utilizar la palabra clave auto en lugar de declarar el tipo. Por ejemplo, considere este código que 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 durante la ejecución de una tarea, el runtime calcula esa excepción en la siguiente llamada a task::get o task::wait, o a una continuación basada en tareas. Para obtener más información sobre el mecanismo de control de excepciones de tareas, vea Control de excepciones en el runtime de simultaneidad.

Para obtener un ejemplo que utiliza la cancelación task, concurrency::task_completion_event, vea Tutorial: Conectar usando tareas y solicitudes HTTP XML. (La clase task_completion_event se describe más adelante en el documento).

SugerenciaSugerencia

Para obtener detalles específicos de las tareas en aplicaciones Tienda Windows, vea Asynchronous programming in C++ y Crear operaciones asincrónicas en C++ para aplicaciones de la Tienda Windows.

[Arriba]

Tareas de continuación

En la programación asincrónica, es muy común que una operación asincrónica, cuando se completa, invoque una segunda operación y le pase datos. Tradicionalmente, esto se hace utilizando métodos de devolución de llamada. En el runtime de simultaneidad, tareas de continuación proporcionan la misma funcionalidad. Una tarea de continuación (o, simplemente, una continuación) es una tarea asincrónica invocada por otra tarea, que se denomina antecedente, cuando se completa el antecedente. Mediante continuaciones se puede:

  • Pasar datos del antecedente a la continuación.

  • Especifica las condiciones precisas en las que se invocará o no la continuación.

  • Cancelar una continuación antes de iniciarse o, de manera cooperativa, mientras se está ejecutando.

  • Proporcionar sugerencias sobre cómo se debería programar la continuación. (Este tema solo atañe a aplicaciones Tienda Windows). Para obtener más información, vea Crear operaciones asincrónicas en C++ para aplicaciones de la Tienda Windows.)

  • Invocar varias continuaciones desde el mismo antecedente.

  • Invocar una continuación cuando se completa una parte o la totalidad de los antecedentes.

  • Encadenar las continuaciones una tras otra hasta cualquier longitud.

  • Use 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 comprima un archivo después de que la primera tarea lo lea desde el disco.

En el ejemplo siguiente se modifica el anterior para utilizar el método concurrency::task::then para programar una continuación que imprima 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 a cualquier longitud. Una tarea también puede tener varias continuaciones. El ejemplo siguiente muestra una cadena de continuación básica que incrementa tres veces 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 cancelación, esta tarea se ejecuta antes de la continuación subsiguiente. Esta técnica se conoce como desencapsulado asincrónico. El desencapsulado asincrónico es útil cuando se desea realizar trabajo adicional en segundo plano, pero no se desea que la tarea actual bloquee el subproceso actual. (Esto suele ocurrir en aplicaciones de Tienda Windows , donde las continuaciones pueden ejecutarse en el subproceso de la interfaz de usuario). El ejemplo siguiente muestra 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
*/
Nota importanteImportante

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 la tarea anidada se complete.Es decir, la continuación realiza el desencapsulado de la tarea anidada.

[Arriba]

Continuaciones basadas en valores frente a continuaciones basadas en tareas

Dado un objeto task cuyo tipo de valor devuelto es T, 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 denomina continuación basada en valores. Una continuación basada en valores se programa para la ejecución cuando la tarea antecedente se completa sin errores y no se cancela. Una continuación que toma el tipo task<T> como su parámetro se denomina continuación basada en tareas. Una continuación basada e tareas se programa siempre para la ejecución cuando finaliza la tarea antecedente, incluso cuando se cancela la tarea antecedente o se produce una excepción. Puede llamar a task::get para obtener el resultado de la tarea antecedente. Si la tarea anterior se cancela, task::get produce concurrency::task_canceled. Si la tarea anterior produjo una excepción, task::get vuelve a producir esa excepción. Una continuación basada en tareas no se marca como cancelada cuando se cancela su tarea antecedente.

[Arriba]

Componer tareas

En esta sección se describen las funciones concurrency::when_all y concurrency::when_any, que pueden ayudarle a crear varias tareas para implementar patrones comunes.

La función when_all

La función when_all produce una tarea que se completa tras una serie de tareas completadas. Esta función devuelve un objeto std::vector que contiene el resultado de cada tarea en el conjunto. En el siguiente ejemplo básico se utiliza 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.
*/
NotaNota

Las tareas que pasa a when_all deben ser uniformes.Es decir, todos deben devolver el mismo tipo.

También puede usar la sintaxis && para producir una tarea que se complete después de que se complete un conjunto de tareas, como se muestra en el ejemplo siguiente.

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 la finalización de un conjunto de tareas. En el ejemplo siguiente se modifica el anterior para imprimir la suma de las tres tareas cada una de las cuales genera un 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.
*/

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

Si cualquier tarea en un conjunto de tareas está cancelada o produce una excepción, when_all se completa inmediatamente y no espera a que las tareas restantes finalicen. Si se produce una excepción, el runtime vuelve a producir la excepción cuando se llama a task::get o task::wait en el objeto de tarea que when_all devuelve. Si se lanza de más de una tarea, el runtime elige una de ellas. Por consiguiente, asegúrese de observar todas las excepciones cuando todas las tareas se han completado; una excepción de tarea no controlada hace que la aplicación finalice.

Aquí se muestra una función de utilidad que puede utilizar para garantizar que el programa sigue todas las excepciones. Para cada tarea en el intervalo proporcionado, observe_all_exceptions desencadena cualquier excepción que se produjo para volver a producir la excepción y después se pasa esa excepción.

// 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 una aplicación Tienda Windows que utilice C++ y XAML, y escriba un conjunto de archivos en disco. El ejemplo siguiente muestra cómo utilizar when_all y observe_all_exceptions para garantizar que el programa observa 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>
    
  2. 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);
    
  3. 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.
        });
    }
    
  4. En MainPage.xaml.cpp, implemente WriteFilesAsync como se muestra en el ejemplo.

SugerenciaSugerencia

when_all es una función sin bloqueos que produce task como resultado.A diferencia de task::wait, es seguro llamar a esta función en una aplicación Tienda Windows en el subproceso de ASTA (aplicación STA).

[Arriba]

La función when_any

La función when_any produce una tarea que se completa cuando se completa 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 esa 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, todas las cuales deben finalizar y utilizar la función when_any para procesar los resultados cuando finalice cada operación. Finalizada una operación, puede iniciar una o más tareas adicionales.

  • Operaciones limitadas. Puede usar la función when_any para extender el escenario anterior si limita 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 termina después de un momento concreto.

Como ocurre con when_all, es habitual usar una continuación que tenga when_any para realizar una acción cuando finalice la primera tarea de un conjunto. En el siguiente ejemplo básico se utiliza when_any para crear una tarea que se completa al completarse 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.

NotaNota

Como ocurre con when_all, las tareas que se pasan a when_any deben devolver todas el mismo tipo.

También puede usar la sintaxis || para generar una tarea que se complete después de que se complete la primera tarea en un conjunto de tareas, como se muestra en el ejemplo siguiente.

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

SugerenciaSugerencia

Como ocurre con when_all, when_any no produce bloqueos y es seguro para llamadas en una aplicación Tienda Windows en el subproceso de ASTA.

[Arriba]

Ejecución retrasada de una tarea

A veces 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 la programación asincrónica, quizás tenga que iniciar una tarea como respuesta a un evento de finalización de E/S.

Dos maneras de lograrlo son usar una continuación o iniciar una tarea, y esperar un evento dentro de la función de trabajo de la tarea. No obstante, existen casos donde no es posible utilizar una de estas técnicas. Por ejemplo, para crear una continuación, debe tener la tarea anterior. Sin embargo, si no tiene la tarea anterior, puede crear un evento de finalización de la tarea y después encadenar ese evento de finalización a la tarea anterior cuando esté disponible. Además, puesto que una tarea en espera también bloquea un subproceso, puede utilizar eventos de finalización de la tarea para realizar el trabajo cuando una operación asincrónica finaliza y libera un subproceso.

La clase concurrency::task_completion_event ayuda a simplificar esta composición de tareas. Como la clase task, 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 utilizar el modificador const. Normalmente, un objeto task_completion_event se proporciona a un subproceso o tarea que lo señalarán cuando el valor esté disponible. Al mismo tiempo, una o más tareas se establecen como agentes de escucha de ese evento. Cuando se establece el evento, las tareas del agente de escucha se completan y sus continuaciones se programan para ejecutarse.

Para obtener un ejemplo que utiliza task_completion_event para implementar una tarea que se completa después de un retraso, vea Cómo: Crear una tarea que se complete después de un retardo.

[Arriba]

Grupos de tareas

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

La biblioteca PPL utiliza 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. Como la clase task, la función de trabajo tiene forma de una función lambda, puntero a función u objeto de función. Normalmente no es necesario trabajar con objetos task_handle directamente. En su lugar, se pasan las funciones de trabajo a un grupo de tareas, y el grupo de tareas crea y administra los objetos task_handle.

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

Nota importanteImportante

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, se recomienda usarla en lugar de la clase structured_task_group si es posible.En el tema Algoritmos paralelos se describe con mayor detalle parallel_invoke.

Use parallel_invoke cuando tenga varias tareas independientes que desee ejecutar al mismo tiempo y deba esperar a que todas las tareas finalicen antes de continuar. Esta técnica suele denominarse paralelismo de bifurcación y unión. Use task_group cuando tenga varias tareas independientes que desee ejecutar al mismo tiempo, pero desee esperar hasta que las tareas finalicen posteriormente. Por ejemplo, puede agregar tareas a un objeto task_group y esperar hasta que las tareas finalicen en otra función o desde otro subproceso.

Los grupos de tareas admiten el concepto de cancelación. La cancelación permite señalar a todas las tareas activas que desea cancelar la operación en su conjunto. La cancelación también impide que se inicien las tareas que todavía no se hayan iniciado. 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 producir una excepción desde una tarea y controlar esa excepción cuando se espera hasta que finaliza el grupo de tareas asociado. Para obtener más información sobre este modelo de control de excepciones, vea Control de excepciones en el runtime de simultaneidad.

[Arriba]

Comparación entre task_group y structured_task_group

Aunque se recomienda usar task_group o parallel_invoke en lugar de la clase structured_task_group, hay casos donde puede ser preferible usar structured_task_group; por ejemplo, cuando se escribe un algoritmo paralelo que realiza un número de tareas variable o que necesita 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 subprocesos. Por tanto, puede agregar tareas a un objeto task_group desde varios subprocesos y esperar o cancelar un objeto task_group desde varios subprocesos. La creación y destrucción de un objeto structured_task_group deben realizarse en el mismo ámbito léxico. Además, todas las operaciones sobre un objeto structured_task_group deben realizarse 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 o comprobar la cancelación en cualquier momento.

Puede ejecutar tareas adicionales en un objeto task_group después de llamar al método concurrency::task_group::wait o concurrency::task_group::run_and_wait. Por el contrario, si ejecuta tareas adicionales 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 es indefinido.

Puesto que la clase structured_task_group no se sincronizado entre subprocesos, tiene menos sobrecarga de ejecución que la clase task_group. Por tanto, si el problema no necesita que se programe trabajo desde varios subprocesos y no se puede usar el algoritmo parallel_invoke, la clase structured_task_group puede ayudarle 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 ser destruido antes de que el objeto externo finalizado. La clase task_group no necesita que los grupos de tareas anidadas finalicen antes de que finalice el grupo externo.

Los grupos de tareas no estructurados y los grupos de tareas estructurados usan los identificadores de tareas de maneras diferentes. Puede pasar funciones de trabajo directamente a un objeto task_group; el objeto task_group creará y administrará el identificador de tareas. La clase structured_task_group necesita administrar un objeto task_handle por cada tarea. Cada objeto task_handle debe seguir siendo válido mientras dure su objeto structured_task_group asociado. Use la función concurrency::make_task para crear un objeto task_handle, como se muestra 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 tareas para aquellos casos en los que tiene un número variable de tareas, use una rutina de asignación de pila como _malloca o una clase contenedora 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.

[Arriba]

Ejemplo

En el siguiente ejemplo básico se muestra cómo trabajar con grupos de tareas. En este ejemplo se usa el algoritmo parallel_invoke para realizar dos tareas simultáneamente. Cada tarea agrega subtareas a un objeto task_group. Observe que la clase task_group permite que varias tareas le agreguen tareas simultáneamente.

// 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 continuación, se muestra la salida de este ejemplo:

  

Puesto que el algoritmo parallel_invoke ejecuta las tareas simultáneamente, el orden de los mensajes de salida podría variar.

Para obtener ejemplos completos en los que se muestra cómo usar el algoritmo parallel_invoke, vea 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 obtener un ejemplo completo en el que se usa la clase task_group para implementar características asincrónicas, vea Tutorial: Implementar futuros.

[Arriba]

Programación sólida

Asegúrese de que entiende el rol de cancelación y el 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 evita que se ejecuten las tareas secundarias. Esto puede producir problemas si una de las tareas secundarias realiza una operación que es importante para la aplicación, como liberar un recurso. Además, si una tarea secundaria produce una excepción, esa excepción podría propagarse a través de un destructor de objeto y provocar un comportamiento no definido en la aplicación. Para obtener un ejemplo que muestra estos puntos, vea la sección Comprender cómo afectan la cancelación y el control de excepciones a la destrucción de objetos del documento de procedimientos recomendados de la Biblioteca de patrones de procesamiento paralelo. Para obtener más información sobre los modelos de cancelación y control de excepciones en PPL, vea Cancelación en la biblioteca PPL y Control de excepciones en el runtime de simultaneidad.

[Arriba]

Temas relacionados

Título

Descripción

Cómo: 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.

Cómo: 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.

Cómo: Crear una tarea que se complete después de un retardo

Muestra cómo utilizar task, cancellation_token_source, cancellation_token y las clases 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 obtener mejoras.

Parallel Patterns Library (PPL)

Describe la biblioteca PPL, que proporciona un modelo de programación imperativo para desarrollar aplicaciones simultáneas.

Reference

task (Clase) (Motor en tiempo de ejecución de simultaneidad)

task_completion_event (Clase)

when_all (Función)

when_any (Función)

task_group (Clase)

parallel_invoke (Función)

structured_task_group (Clase)