Parallélisme des tâches (runtime d’accès concurrentiel)

Dans le runtime d’accès concurrentiel, une tâche est une unité de travail qui effectue un travail spécifique et s’exécute généralement en parallèle avec d’autres tâches. Une tâche peut être décomposée en tâches supplémentaires plus affinées organisées en groupe de tâches.

Vous utilisez des tâches quand vous écrivez du code asynchrone et que vous voulez qu’une opération se produise une fois que l’opération asynchrone est terminée. Par exemple, vous pouvez utiliser une tâche pour lire de manière asynchrone à partir d’un fichier, puis utiliser une autre tâche , une tâche de continuation, expliquée plus loin dans ce document, pour traiter les données une fois qu’elles sont disponibles. À l’inverse, vous pouvez utiliser des groupes de tâches pour décomposer un travail parallèle en éléments plus petits. Par exemple, supposons que vous ayez un algorithme récursif qui divise le travail restant en deux partitions. Vous pouvez utiliser des groupes de tâches pour exécuter ces partitions simultanément, puis attendre que le travail divisé soit terminé.

Conseil

Lorsque vous souhaitez appliquer la même routine à chaque élément d’une collection en parallèle, utilisez un algorithme parallèle, tel que concurrency ::p arallel_for, au lieu d’une tâche ou d’un groupe de tâches. Pour plus d’informations sur les algorithmes parallèles, consultez Algorithmes parallèles.

Points clés

  • Quand vous passez des variables à une expression lambda par référence, vous devez vous assurer que la durée de vie de cette variable persiste jusqu'à ce que la tâche se termine.

  • Utilisez des tâches (la classe concurrency ::task ) lorsque vous écrivez du code asynchrone. La classe task utilise le pool de threads Windows en tant que planificateur, et non le runtime d’accès concurrentiel.

  • Utilisez des groupes de tâches (la classe concurrency ::task_group ou l’algorithme concurrency ::p arallel_invoke ) lorsque vous souhaitez décomposer le travail parallèle en morceaux plus petits, puis attendez que ces petits morceaux se terminent.

  • Utilisez la méthode concurrency ::task ::then pour créer des continuations. Une continuation est une tâche qui s’exécute de manière asynchrone après la fin d’une autre tâche. Vous pouvez connecter autant de continuations que vous voulez pour former une chaîne de travail asynchrone.

  • Une continuation basée sur des tâches est toujours planifiée pour l’exécution quand la tâche antécédente se termine, même quand la tâche antécédente est annulée ou lève une exception.

  • Utilisez concurrency ::when_all pour créer une tâche qui se termine une fois chaque membre d’un ensemble de tâches terminé. Utilisez concurrency ::when_any pour créer une tâche qui se termine une fois qu’un membre d’un ensemble de tâches est terminé.

  • Les tâches et les groupes de tâches peuvent participer au mécanisme d’annulation de la bibliothèque de modèles parallèles (PPL). Pour plus d’informations, consultez Annulation dans la bibliothèque PPL.

  • Pour savoir comment le runtime gère les exceptions levées par des tâches et des groupes de tâches, consultez Gestion des exceptions.

Dans ce document

Utilisation d’expressions lambda

En raison de leur syntaxe concise, les expressions lambda constituent une manière courante de définir le travail effectué par les tâches et les groupes de tâches. Voici quelques conseils d'utilisation :

  • Étant donné que les tâches s’exécutent généralement sur des threads d’arrière-plan, tenez compte de la durée de vie des objets quand vous capturez des variables dans des expressions lambda. Quand vous capturez une variable par valeur, une copie de cette variable est effectuée dans le corps de l'expression lambda. Quand vous capturez par référence, aucune copie n'est effectuée. Par conséquent, vérifiez que la durée de vie d'une variable que vous capturez par référence est supérieure à celle de la tâche qui l'utilise.

  • Lorsque vous transmettez une expression lambda à une tâche, ne capturez pas les variables allouées sur la pile par référence.

  • Soyez explicite sur les variables que vous capturez dans des expressions lambda afin de pouvoir identifier ce que vous capturez par valeur par référence. C'est pour cela que nous vous recommandons de ne pas utiliser les options [=] ou [&] pour les expressions lambda.

Il est courant qu'une seule tâche dans une chaîne de continuation s'assigne à une variable et qu'une autre tâche lise cette variable. Vous ne pouvez pas capturer par valeur, car chaque tâche de continuation contiendrait une copie différente de la variable. Pour les variables allouées par pile, vous ne pouvez pas également capturer par référence, car la variable peut ne plus être valide.

Pour résoudre ce problème, utilisez un pointeur intelligent, tel que std ::shared_ptr, pour encapsuler la variable et passer le pointeur intelligent par valeur. Ainsi, l'objet sous-jacent peut être assigné et lu, et il survit aux tâches qui l'utilisent. Utilisez cette technique, même quand la variable est un pointeur ou un handle compté par référence (^) à un objet Windows Runtime. Voici un exemple de base :

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

Pour plus d’informations sur les expressions lambda, consultez Expressions Lambda.

Classe de tâche

Vous pouvez utiliser la classe concurrency ::task pour composer des tâches dans un ensemble d’opérations dépendantes. Ce modèle de composition est pris en charge par la notion de continuations. Une continuation permet l’exécution du code lorsque la tâche précédente, ou antécédente, se termine. Le résultat de la tâche antécédente est transmis comme entrée à une ou plusieurs tâches de continuation. Quand une tâche antécédente est terminée, toutes les tâches de continuation qui l'attendent sont planifiées pour l'exécution. Chaque tâche de continuation reçoit une copie du résultat de la tâche antécédente. À leur tour, ces tâches de continuation peuvent également être des tâches antécédentes pour d’autres continuations, créant ainsi une chaîne de tâches. Les continuations vous aident à créer des chaînes de longueur arbitraire de tâches qui ont des dépendances spécifiques entre elles. De plus, une tâche peut participer à l’annulation avant le démarrage d’une tâche ou de manière coopérative pendant son exécution. Pour plus d’informations sur ce modèle d’annulation, consultez Annulation dans la bibliothèque PPL.

task est une classe de modèle. Le paramètre de type T est le type du résultat produit par la tâche. Ce type peut être void si la tâche ne retourne pas de valeur. T ne peut pas utiliser le modificateur const.

Lorsque vous créez une tâche, vous fournissez une fonction de travail qui exécute le corps de la tâche. Cette fonction de travail se présente sous la forme d'une fonction lambda, de pointeur de fonction ou d'objet de fonction. Pour attendre qu’une tâche se termine sans obtenir le résultat, appelez la méthode concurrency ::task ::wait . La task::wait méthode retourne une valeur concurrency ::task_status qui décrit si la tâche a été terminée ou annulée. Pour obtenir le résultat de la tâche, appelez la méthode concurrency ::task ::get . Cette méthode appelle task::wait pour attendre que la tâche se termine, et bloque donc l'exécution du thread actuel jusqu'à ce que le résultat soit disponible.

L'exemple suivant montre comment créer une tâche, attendre son résultat et afficher sa valeur. Les exemples fournis dans cette documentation utilisent des fonctions lambda car ils fournissent une syntaxe plus concise. Toutefois, vous pouvez également utiliser des pointeurs de fonction et des objets de fonction quand vous utilisez des tâches.

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

Lorsque vous utilisez la fonction concurrency ::create_task, vous pouvez utiliser la auto mot clé au lieu de déclarer le type. Par exemple, prenons ce code qui crée et imprime la matrice d'identité :

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

Vous pouvez utiliser la fonction create_task pour créer l'opération équivalente.

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 une exception est levée pendant l’exécution d’une tâche, le runtime marshale cette exception dans l’appel suivant vers task::get ou task::waitvers une continuation basée sur des tâches. Pour plus d’informations sur le mécanisme de gestion des exceptions de tâche, consultez Gestion des exceptions.

Pour obtenir un exemple qui utilise task, concurrency ::task_completion_event, annulation, consultez procédure pas à pas : Connecter ing Using Tasks and XML HTTP Requests. (La classe task_completion_event est décrite ultérieurement dans ce document.)

Conseil

Pour en savoir plus sur les tâches spécifiques aux applications UWP, consultez Programmation asynchrone en C++ et Création d’opérations asynchrones en C++ pour les applications UWP.

Tâches de continuation

En programmation asynchrone, il est très courant pour une opération asynchrone, une fois terminée, d'appeler une deuxième opération et de lui passer des données. Pour cela, il est d'usage d'avoir recours à des méthodes de rappel. Dans le runtime d’accès concurrentiel, la même fonctionnalité est fournie par les tâches de continuation. Une tâche de continuation (également connue sous le nom de continuation) est une tâche asynchrone appelée par une autre tâche, connue sous le nom d’antécédent, lorsque l’antécédent se termine. En utilisant les continuations, vous pouvez :

  • passer des données de l'antécédent à la continuation ;

  • spécifier les conditions précises dans lesquelles la continuation doit être ou non ;

  • annuler une continuation avant son démarrage ou pendant son exécution de manière coopérative ;

  • fournir des conseils sur la manière de planifier la continuation ; (Cela s’applique uniquement aux applications plateforme Windows universelle (UWP). Pour plus d’informations, consultez Création d’opérations asynchrones en C++ pour les applications UWP.)

  • appeler plusieurs continuations depuis le même antécédent ;

  • appeler une continuation quand certains ou tous les antécédents se terminent ;

  • chaîner des continuations les unes à la suite des autres qu'elle qu'en soit la longueur ;

  • utiliser une continuation pour gérer des exceptions levées par l'antécédent.

Ces fonctionnalités vous permettent d’exécuter une ou plusieurs tâches quand la première tâche se termine. Par exemple, vous pouvez créer une continuation qui compresse un fichier une fois que la première tâche le lit à partir du disque.

L’exemple suivant modifie le précédent pour utiliser la méthode concurrency ::task ::then pour planifier une continuation qui imprime la valeur de la tâche antécédente lorsqu’elle 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
*/

Vous pouvez chaîner et imbriquer des tâches jusqu'à n'importe quelle longueur. Une tâche peut également avoir plusieurs continuations. L’exemple suivant illustre une chaîne de continuation de base qui incrémente la valeur de la tâche précédente trois fois.

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

Une continuation peut également retourner une autre tâche. En l’absence d’annulation, cette tâche est exécutée avant la continuation suivante. Cette technique est appelée désencapsulation asynchrone. Le désencapsulage asynchrone s’avère utile quand vous voulez effectuer du travail supplémentaire en arrière-plan, mais sans que la tâche en cours bloque le thread actuel. (Ceci est courant dans les applications UWP, où les continuations peuvent s’exécuter sur le thread d’interface utilisateur). L’exemple suivant montre trois tâches. La première tâche retourne une autre tâche qui est exécutée avant une tâche de continuation.

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

Important

Quand une continuation d’une tâche retourne une tâche imbriquée de type N, la tâche qui en résulte a le type N, et non task<N>, et elle se termine quand la tâche imbriquée se termine. En d’autres termes, la continuation désencapsule la tâche imbriquée.

Continuations basées sur des valeurs et basées sur des tâches

Étant donné un objet task dont le type de retour est T, vous pouvez fournir une valeur de type T ou task<T> à ses tâches de continuation. Une continuation qui prend le type T est appelée continuation basée sur les valeurs. Une continuation basée sur des valeurs est planifiée pour l‘exécution quand la tâche antécédente se termine sans erreur et qu‘elle n‘est pas annulée. Une continuation qui prend le type task<T> en tant que paramètre est appelée continuation basée sur des tâches. Une continuation basée sur des tâches est toujours planifiée pour l’exécution quand la tâche antécédente se termine, même quand la tâche antécédente est annulée ou lève une exception. Vous pouvez alors appeler task::get pour obtenir le résultat de la tâche antécédente. Si la tâche antécédente a été annulée, task::get lève l’accès concurrentiel ::task_canceled. Si la tâche antécédente a levé une exception, task::get lève à nouveau cette exception. Une continuation basée sur des tâches n‘est pas marquée comme annulée quand sa tâche antécédente est annulée.

Composition des tâches

Cette section décrit les fonctions concurrency ::when_all et concurrency ::when_any , qui peuvent vous aider à composer plusieurs tâches pour implémenter des modèles courants.

Fonction when_all

La fonction when_all produit une tâche qui s'exécute une fois qu'un ensemble de tâches est terminé. Cette fonction retourne un objet std ::vector qui contient le résultat de chaque tâche dans l’ensemble. L'exemple de base suivant utilise when_all pour créer une tâche qui représente l'achèvement de trois autres tâches.

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

Remarque

Les tâches que vous passez à when_all doivent être uniformes. En d'autres termes, elles doivent toutes retourner le même type.

Vous pouvez également utiliser la syntaxe && pour produire une tâche qui s'exécute une fois qu'un ensemble de tâches est terminé, comme illustré dans l'exemple suivant.

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

Il est courant d'utiliser une continuation avec when_all pour exécuter une action une fois qu'un ensemble de tâches est terminé. L'exemple suivant modifie le précédent pour imprimer la somme des trois tâches qui produisent chacune un résultat 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.
*/

Dans cet exemple, vous pouvez également spécifier task<vector<int>> pour produire une continuation basée sur des tâches.

Si une tâche d'un ensemble de tâches est annulée ou lève une exception, when_all se termine immédiatement et n'attend pas que les tâches restantes se terminent. Si une exception est levée, le runtime lève à nouveau l’exception quand vous appelez task::get ou task::wait sur l’objet de tâche que when_all retourne. Si plusieurs tâches lèvent une exception, le runtime en choisit une. Par conséquent, vérifiez que vous observez toutes les exceptions une fois toutes les tâches terminées ; une exception de tâche non gérée entraîne l’arrêt de l’application.

Voici une fonction utilitaire que vous pouvez utiliser pour vous assurer que votre programme observe toutes les exceptions. Pour chaque tâche incluse dans la plage fournie, observe_all_exceptions déclenche toute exception qui s’est produite pour être à nouveau levée, puis avale cette exception.

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

Considérez une application UWP qui utilise C++ et XAML et écrit un ensemble de fichiers sur disque. L'exemple suivant montre comment utiliser when_all et observe_all_exceptions pour vous assurer que le programme observe toutes les exceptions.

// 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();
        }
    });
}
Pour exécuter cet exemple
  1. Dans MainPage.xaml, ajoutez un contrôle Button.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. Dans MainPage.xaml.h, ajoutez ces déclarations anticipées à la section private de la déclaration de classe MainPage.
void Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
concurrency::task<void> WriteFilesAsync(Windows::Storage::StorageFolder^ folder, const std::vector<std::pair<Platform::String^, Platform::String^>>& fileContents);
  1. Dans MainPage.xaml.cpp, implémentez le gestionnaire d'événements 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. Dans MainPage.xaml.cpp, implémentez WriteFilesAsync comme indiqué dans l'exemple.

Conseil

when_all est une fonction non bloquante qui produit un task en tant que résultat. Contrairement à task ::wait, il est sûr d’appeler cette fonction dans une application UWP sur le thread ASTA (Application STA).

Fonction when_any

La fonction when_any produit une tâche qui se termine quand la première tâche d'un ensemble de tâches se termine. Cette fonction retourne un objet std ::p air qui contient le résultat de la tâche terminée et l’index de cette tâche dans l’ensemble.

La fonction when_any s'avère particulièrement utile dans les scénarios suivants :

  • Opérations redondantes. Considérez un algorithme ou une opération pouvant être exécutée plusieurs façons. Vous pouvez utiliser la fonction when_any pour sélectionner l'opération qui se termine en premier et annuler les opérations restantes.

  • Opérations entrelacées. Vous pouvez démarrer plusieurs opérations qui doivent toutes se terminer et utiliser la fonction when_any pour traiter les résultats à mesure que chaque opération se termine. Lorsqu’une opération se termine, vous pouvez démarrer une ou plusieurs autres tâches.

  • Opérations limitées. Vous pouvez utiliser la fonction when_any pour étendre le scénario précédent en limitant le nombre d'opérations simultanées.

  • Opérations expirées. Vous pouvez utiliser la fonction when_any pour choisir une ou plusieurs tâches et une tâche qui se termine après un délai spécifique.

Comme avec when_all, il est courant d'utiliser une continuation avec when_any pour effectuer une action quand la première tâche d'un ensemble de tâches se termine. L’exemple de base suivant utilise when_any pour créer une tâche qui se termine quand la première des trois autres tâches se termine.

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

Dans cet exemple, vous pouvez également spécifier task<pair<int, size_t>> pour produire une continuation basée sur des tâches.

Remarque

Comme avec when_all, les tâches que vous passez à when_any doivent toutes retourner le même type.

Vous pouvez également utiliser la syntaxe || pour produire une tâche qui s’exécute une fois que la première tâche d’un ensemble de tâches est terminée, comme illustré dans l’exemple suivant.

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

Conseil

Comme avec when_all, when_any n’est pas bloquant et est sûr d’appeler dans une application UWP sur le thread ASTA.

Exécution différée de la tâche

Il est parfois nécessaire de retarder l’exécution d’une tâche jusqu’à ce qu’une condition soit satisfaite, ou de démarrer une tâche en réponse à un événement externe. Par exemple, en programmation asynchrone, vous devrez peut-être démarrer une tâche en réponse à un événement d’achèvement d’E/S.

Deux façons d’y parvenir sont d’utiliser une continuation ou de démarrer une tâche et d’attendre un événement à l’intérieur de la fonction de travail de la tâche. Toutefois, il existe des cas où il n'est pas possible d'utiliser l'une de ces techniques. Par exemple, pour créer une continuation, vous devez disposer de la tâche antécédente. Toutefois, si vous n’avez pas la tâche antérieure, vous pouvez créer un événement d’achèvement de tâche et chaîne ultérieurement cet événement d’achèvement à la tâche antérieure lorsqu’elle devient disponible. De plus, puisqu’une tâche en attente bloque également un thread, vous pouvez utiliser des événements d’achèvement de tâche pour effectuer un travail quand une opération asynchrone se termine et ainsi libérer un thread.

La classe concurrency ::task_completion_event permet de simplifier cette composition de tâches. Comme la classe task, le paramètre de type T est le type du résultat produit par la tâche. Ce type peut être void si la tâche ne retourne pas de valeur. T ne peut pas utiliser le modificateur const. En règle générale, un objet task_completion_event est fourni à un thread ou une tâche qui le signale quand sa valeur devient disponible. En même temps, une ou plusieurs tâches sont définies en tant qu’écouteurs de cet événement. Quand l’événement est défini, les tâches de l’écouteur s’achèvent et leurs continuations sont planifiées pour s’exécuter.

Pour obtenir un exemple qui utilise task_completion_event pour implémenter une tâche qui se termine après un délai, consultez Comment : créer une tâche qui se termine après un délai.

Groupes de tâches

Un groupe de tâches organise une collection de tâches. Les groupes de tâches transmettent des tâches vers une file d'attente de vol de travail. Le planificateur supprime les tâches de cette file d’attente et les exécute sur les ressources informatiques disponibles. Après avoir ajouté des tâches à un groupe de tâches, vous pouvez attendre que toutes les tâches se terminent ou annuler celles qui n’ont pas encore commencé.

La bibliothèque PPL utilise les classes concurrency ::task_group et concurrency ::structured_task_group pour représenter des groupes de tâches et la classe concurrency ::task_handle pour représenter les tâches qui s’exécutent dans ces groupes. La classe task_handle encapsule le code qui effectue le travail. Comme la classe task, la fonction de travail se présente sous la forme d'une fonction lambda, de pointeur de fonction ou d'objet de fonction. En général, vous n'avez pas besoin d'utiliser directement des objets task_handle. Au lieu de cela, vous passez des fonctions de travail à un groupe de tâches et celui-ci crée et gère les objets task_handle.

Le PPL divise les groupes de tâches en deux catégories : les groupes de tâches non structurés et les groupes de tâches structurés. La bibliothèque de modèles parallèles utilise la classe task_group pour représenter les groupes de tâches non structurés et la classe structured_task_group pour représenter les groupes de tâches structurés.

Important

Le PPL définit également l’algorithme concurrency ::p arallel_invoke , qui utilise la structured_task_group classe pour exécuter un ensemble de tâches en parallèle. Étant donné que l'algorithme parallel_invoke a une syntaxe plus concise, nous vous recommandons de l'utiliser à la place de la classe structured_task_group dès que possible. La rubrique Algorithmes parallèles décrit parallel_invoke en détail.

Utilisez parallel_invoke quand vous avez plusieurs tâches indépendantes à exécuter en même temps, et que vous devez attendre que toutes les tâches soient terminées avant de continuer. Cette technique est souvent appelée fork et jointure parallélisme. Utilisez task_group quand vous avez plusieurs tâches indépendantes à exécuter en même temps, mais que vous voulez attendre plus tard que les tâches se terminent. Par exemple, vous pouvez ajouter des tâches à un objet task_group et attendre qu’elles se terminent dans une autre fonction ou à partir d’un autre thread.

Les groupes de tâches prennent en charge le concept d’annulation. L’annulation vous permet de signaler à toutes les tâches actives que vous voulez annuler l’opération globale. L'annulation empêche également les tâches qui n'ont pas encore démarré de démarrer. Pour plus d’informations sur l’annulation, consultez Annulation dans la bibliothèque PPL.

Le runtime fournit également un modèle de gestion des exceptions qui vous permet de lever une exception à partir d'une tâche et de gérer cette exception quand vous attendez que le groupe de tâches associé se termine. Pour plus d’informations sur ce modèle de gestion des exceptions, consultez Gestion des exceptions.

Comparaison de task_group à structured_task_group

Bien que nous vous recommandions d'utiliser task_group ou parallel_invoke au lieu de la classe structured_task_group, dans certains cas, vous pouvez utiliser structured_task_group, par exemple, quand vous écrivez un algorithme parallèle qui effectue un nombre variable de tâches ou qui requiert la prise en charge de l'annulation. Cette section explique les différences entre les classes task_group et structured_task_group.

La classe task_group est thread-safe. Ainsi, vous pouvez ajouter des tâches à un objet task_group à partir de plusieurs threads et attendre ou annuler un objet task_group à partir de plusieurs threads. La construction et la destruction d'un objet structured_task_group doit se produire dans la même portée lexicale. De plus, toutes les opérations sur un objet structured_task_group doivent se produire sur le même thread. L’exception à cette règle est les méthodes concurrency ::structured_task_group ::cancel et concurrency ::structured_task_group ::is_canceling . Une tâche enfant peut appeler ces méthodes pour annuler le groupe de tâches parent ou vérifier l'annulation à tout moment.

Vous pouvez exécuter des tâches supplémentaires sur un task_group objet après avoir appelé la méthode concurrency ::task_group ::wait ou concurrency ::task_group ::run_and_wait . À l’inverse, si vous exécutez des tâches supplémentaires sur un structured_task_group objet après avoir appelé les méthodes concurrency ::structured_task_group ::wait ou concurrency ::structured_task_group ::run_and_wait , le comportement n’est pas défini.

Étant donné que la classe structured_task_group n’est pas synchronisée entre les threads, elle a moins de charge d’exécution que la classe task_group. Par conséquent, si votre problème ne requiert pas de planification du travail à partir de plusieurs threads et que vous ne pouvez pas utiliser l'algorithme parallel_invoke, la classe structured_task_group peut vous aider à écrire du code plus performant.

Si vous utilisez un seul objet structured_task_group à l’intérieur d’un autre objet structured_task_group, l’objet interne doit se terminer et être détruit avant la fin de l’objet externe. La classe task_group ne requiert pas que des groupes de tâches imbriqués se terminent avant les groupes externes.

Les groupes de tâches non structurés et les groupes de tâches structurés utilisent différemment les handles de tâches. Vous pouvez passer directement des fonctions de travail à un objet task_group ; l’objet task_group crée et gère le handle de tâche pour vous. La classe structured_task_group vous oblige à gérer un objet task_handle pour chaque tâche. Chaque objet task_handle doit rester valide pendant toute la durée de vie de son objet structured_task_group associé. Utilisez la fonction concurrency ::make_task pour créer un task_handle objet, comme illustré dans l’exemple de base suivant :

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

Pour gérer les handles de tâches dans les cas où vous avez un nombre variable de tâches, utilisez une routine d’allocation de pile telle que _malloca ou une classe de conteneur, telle que std ::vector.

task_group et structured_task_group prennent en charge l'annulation. Pour plus d’informations sur l’annulation, consultez Annulation dans la bibliothèque PPL.

Exemple

L’exemple de base suivant montre comment utiliser des groupes de tâches. Cet exemple utilise l'algorithme parallel_invoke pour effectuer deux tâches simultanément. Chaque tâche ajoute des sous-tâches à un objet task_group. Notez que la classe task_group permet à plusieurs tâches d’y ajouter des tâches simultanément.

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

Voici un exemple de sortie pour cet exemple :

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

Étant donné que l'algorithme parallel_invoke exécute des tâches simultanément, l'ordre des messages de sortie peut varier.

Pour obtenir des exemples complets qui montrent comment utiliser l’algorithme parallel_invoke , consultez How to : Use parallel_invoke to Write a Parallel Sort Routine and How to : Use parallel_invoke to Execute Parallel Operations. Pour obtenir un exemple complet qui utilise la task_group classe pour implémenter des futures asynchrones, consultez Procédure pas à pas : implémentation d’futures.

Programmation fiable

Assurez-vous de bien comprendre le rôle de l’annulation et la gestion des exceptions quand vous utilisez des tâches, des groupes de tâches et des algorithmes parallèles. Par exemple, dans une arborescence de travail parallèle, une tâche qui est annulée empêche les tâches enfants de s'exécuter. Des problèmes peuvent alors survenir si l'une des tâches enfants effectue une opération importante pour votre application, comme la libération d'une ressource. De plus, si une tâche enfant lève une exception, cette exception risque de se propager via un destructeur d'objet et d'entraîner un comportement indéfini dans votre application. Pour obtenir un exemple illustrant ces points, consultez la section Comprendre comment la gestion des annulations et des exceptions affecte la destruction d’objets dans les meilleures pratiques du document de la bibliothèque de modèles parallèles. Pour plus d’informations sur les modèles d’annulation et de gestion des exceptions dans ppL, consultez Annulation et Gestion des exceptions.

Intitulé Description
Guide pratique pour utiliser parallel_invoke pour écrire une routine de tri parallèle Montre comment utiliser l'algorithme parallel_invoke pour améliorer les performances de l'algorithme de tri bitonique.
Guide pratique pour utiliser parallel_invoke pour exécuter des opérations parallèles Montre comment utiliser l'algorithme parallel_invoke pour améliorer les performances d'un programme qui effectue plusieurs opérations sur une source de données partagée.
Guide pratique pour créer une tâche qui se termine après un certain délai Montre comment utiliser les taskclasses , cancellation_token_sourceet cancellation_tokentask_completion_event les classes pour créer une tâche qui se termine après un délai.
Procédure pas à pas : implémentation d’objets future Montre comment combiner les fonctionnalités existantes du runtime d'accès concurrentiel pour en optimiser l'usage.
Bibliothèque de modèles parallèles Décrit la bibliothèque de modèles parallèles (PPL), qui fournit un modèle de programmation impérative pour développer des applications simultanées.

Référence

task (Concurrency Runtime), classe

task_completion_event, classe

fonction when_all

fonction when_any

Classe task_group

fonction parallel_invoke

structured_task_group, classe