Paralelismo de tareas (Runtime de simultaneidad)

 

Publicado: julio de 2016

Para obtener la documentación más reciente de Visual Studio 2017 RC, consulte Documentación de Visual Studio 2017 RC.

En el Runtime de simultaneidad, un tarea es una unidad de trabajo que realiza un trabajo específico y normalmente se ejecuta en paralelo con otras tareas. Una tarea se puede descomponer en tareas adicionales, más específicas que se organizan en una 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. Por ejemplo, podría utilizar una tarea para leer de forma asincrónica desde un archivo y, a continuación, 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é disponible. 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.

System_CAPS_ICON_tip.jpg Sugerencia

Cuando desea aplicar la misma rutina para 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 acerca de los algoritmos paralelos, vea algoritmos paralelos.

  • Si pasa variables por referencia a una expresión lambda, debe garantizar que esas variables se conserven hasta que finalice la tarea.

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

  • Usar grupos de tareas (la Concurrency:: task_group clase o Concurrency:: parallel_invoke algoritmo) cuando desee descomponer el trabajo paralelo en partes más pequeñas y, a continuación, espere a que esas partes más pequeñas completar.

  • Utilice la concurrency::task::then método para crear continuaciones. Un continuación es una tarea que se ejecuta de forma asincrónica finalice 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:: HYPERLINK "http://msdn.microsoft.com/library/system.threading.tasks.task.whenall (v=VS.110).aspx" when_all para crear una tarea que finaliza después de todos los miembros de un conjunto de tareas. Use Concurrency:: when_any para crear una tarea que finaliza después de un miembro de un conjunto de tareas.

  • 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 obtener más información, consulte cancelación.

  • Para obtener información sobre cómo controla el runtime las excepciones producidas por tareas y grupos de tareas, consulte Exception Handling.

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 resolver este problema, utilice 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 en tiempo de ejecución. 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.

Puede usar el Concurrency:: Task clase para crear tareas en un conjunto de operaciones dependientes. Este modelo de composición es compatible con la noción de continuaciones. Un código de continuación permite que se ejecute cuando el anterior, o antecedente, complete la tarea. 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 obtener más información acerca de este modelo de cancelación, consulte cancelación.

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, se proporciona un función de trabajo que realiza 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 finalice sin obtener el resultado de una tarea, llame a la concurrency::task::wait método. El task::wait método devuelve un concurrency::task_status valor que describe si la tarea se ha completado o cancelado. Para obtener el resultado de la tarea, llame a la concurrency::task::get método. 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 el Concurrency:: create_task función, puede utilizar el auto (palabra clave) 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 obtener más información sobre el mecanismo de control de excepciones de tareas, consulte Exception Handling.

Para obtener un ejemplo que usa task, Concurrency:: task_completion_event, cancelación, consulte Tutorial: conectarse usando tareas y solicitudes de HTTP XML. (La clase task_completion_event se describe más adelante en este documento).

System_CAPS_ICON_tip.jpg Sugerencia

Para obtener detalles específicos de tareas en Tienda Windows 8.x aplicaciones, consulte programación asincrónica en C++ y crear operaciones asincrónicas en C++ para aplicaciones de tienda Windows.

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 proporciona la misma funcionalidad 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, que se conoce como el antecedente, cuando el antecedente se completa. 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 solo es válido en aplicaciones de la Tienda Windows 8.x. Para obtener más información, consulte crear operaciones asincrónicas en C++ para aplicaciones de tienda Windows.)

  • 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 ejemplo siguiente se modifica el anterior para utilizar el concurrency::task::then método para programar una continuación que se 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 de la Tienda Windows 8.x, 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
*/

System_CAPS_ICON_important.jpg 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.

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 acepta el tipo T se conoce como un continuación basada en el valor. 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 acepta el tipo task<T> como su parámetro se conoce como un basado en tareas de continuación. 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 cancela, task::get produce 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.

Esta sección se describe la Concurrency:: when_all y Concurrency:: when_any funciones, que le ayudarán a crear varias tareas para implementar patrones comunes.

La 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 std::vector objeto que contiene el resultado de cada tarea en el 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.
*/

System_CAPS_ICON_note.jpg 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 especificartask<vector<int>> para producir 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 de la Tienda Windows 8.x 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.
System_CAPS_ICON_tip.jpg 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 un Tienda Windows 8.x aplicación en el subproceso ASTA (Application STA).

La 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 std:: Pair objeto 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.

System_CAPS_ICON_note.jpg 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

System_CAPS_ICON_tip.jpg Sugerencia

Al igual que when_all, when_any no aplica bloqueos y se puede llamar de forma segura en una aplicación de la Tienda Windows 8.x en el subproceso ASTA.

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. Sin embargo, si no tiene la tarea anterior, puede crear un el evento de finalización de tarea y ese evento de finalización de la tarea antecedente la cadena más adelante cuando 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.

El Concurrency:: task_completion_event clase ayuda a simplificar dicha composición de 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 obtener un ejemplo que utiliza task_completion_event para implementar una tarea que finaliza después de un retraso, vea Cómo: crear una tarea que finaliza después de un retraso.

Un grupo de tareas organiza un conjunto 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 la Concurrency:: task_group y Concurrency:: structured_task_group clases que representan grupos de tareas y el Concurrency:: task_handle clase 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 estructurados y la estructura de grupos de tareas. 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.

System_CAPS_ICON_important.jpg Importante

PPL también define la Concurrency:: parallel_invoke algoritmo que utiliza la structured_task_group clase 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. El tema algoritmos paralelos describe parallel_invoke con mayor detalle.

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 unión y bifurcación paralelismo. 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, consulte cancelación.

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 obtener más información acerca de este modelo de control de excepciones, vea Exception Handling.

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 es la concurrency::structured_task_group::cancel y concurrency::structured_task_group::is_canceling métodos. 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 tareas adicionales un task_group objeto después de llamar a la concurrency::task_group::wait o concurrency::task_group::run_and_wait método. Por el contrario, si ejecuta tareas adicionales en un structured_task_group objeto después de llamar a la concurrency::structured_task_group::wait o concurrency::structured_task_group::run_and_wait métodos, el comportamiento es 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. Utilice la concurrency::make_task función para crear un task_handle de objeto, 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 tarea para los casos donde haya 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, consulte cancelación.

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 obtener ejemplos completos que muestran cómo utilizar el parallel_invoke algoritmo, 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 que usa el task_group clase para implementar futuros asincrónicas, vea Tutorial: implementar futuros.

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 obtener un ejemplo que ilustra estos puntos, consulte la entender cómo la cancelación y la excepción control afecta a la destrucción de objetos sección de procedimientos recomendados en el documento de Parallel Patterns Library. Para obtener más información acerca de los modelos de control de excepciones en la biblioteca PPL y cancelación, consulte cancelación y Exception Handling.

TítuloDescripción
Cómo: usar Parallel.Invoke para escribir una rutina de ordenación en paraleloMuestra 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 paralelasMuestra 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 finaliza después de un retrasoMuestra cómo utilizar el task, cancellation_token_source, cancellation_token, y task_completion_event las clases para crear una tarea que finaliza después de un retraso.
Tutorial: Implementar futurosMuestra cómo combinar la funcionalidad existente en el Runtime de simultaneidad para ampliar capacidades.
Biblioteca de modelos paralelos (PPL)Describe la biblioteca PPL, que proporciona un modelo de programación imperativo para desarrollar aplicaciones simultáneas.

tarea (clase) (Runtime 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)

Mostrar: