Partager via


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 se décomposer en tâches supplémentaires plus fines qui sont organisées dans un groupe de tâches.

Vous utilisez les tâches lorsque vous écrivez du code asynchrone et souhaitez 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 façon asynchrone un fichier et utiliser une autre tâche, une tâche de continuation, qui est expliquée plus loin dans ce document, pour traiter les données une fois qu'elles sont disponibles. Inversement, vous pouvez utiliser des groupes de tâches pour décomposer le travail parallèle en éléments plus petits. Par exemple, supposez que vous avez un algorithme récursif qui divise le travail restant en deux partitions. Vous pouvez utiliser des groupes de tâches pour exécuter simultanément ces partitions, puis attendre que le travail divisé se termine.

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::parallel_for à la place 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

  • Lorsque vous passez des variables à une expression lambda par référence, vous devez garantir 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.

  • Utilisez des groupes de tâches (la classe concurrency::task_group ou l'algorithme concurrency::parallel_invoke ) lorsque vous souhaitez décomposer le travail parallèle en éléments plus petits puis attendre que ces unités plus petites 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 façon asynchrone lorsqu'une autre tâche se termine. Vous pouvez connecter plusieurs continuations pour former une chaîne de travail asynchrone.

  • Une continuation à base de tâche est toujours planifiée pour exécution à la fin de l'antécédent, même lorsque celui-ci est annulé ou lève une exception.

  • Utilisez concurrency::when_all pour créer une tâche qui se termine une fois que chaque membre d'un jeu de tâches a terminé. Utilisez concurrency::when_any pour créer une tâche qui se termine une fois qu'un membre d'un jeu de tâches a 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 de modèles parallèles.

  • 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 le runtime d'accès concurrentiel.

Dans ce document

  • Utilisation d'expressions lambda

  • Classe de tâche

  • Tâches de continuation

  • Différences entre les continuations basées sur les valeurs et les continuations basées sur les tâches

  • Composition des tâches

    • Fonction when_all

    • Fonction when_any

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

  • Groupes de tâches

  • Comparaison de task_group et de structured_task_group

  • Exemple

  • Programmation fiable

Utilisation d'expressions lambda

En raison de leur syntaxe succincte, les expressions lambda sont une méthode courante de définir le travail qui est exécuté par des tâches et des groupes de tâches. Voici quelques conseils d'utilisation :

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

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

  • Soyez explicite sur les variables que vous capturez dans les expressions lambda de sorte que vous puissiez identifier ce que vous capturez par valeur plutôt que par référence. C'est pourquoi nous vous recommandons de ne pas utiliser les options [=] ou [&] pour les expressions lambda.

La situation dans laquelle une tâche d'une chaîne de continuation est assignée à une variable et où une autre tâche lit cette variable est un modèle commun. Vous ne pouvez pas capturer par valeur, car chaque tâche de continuation détient une copie différente de la variable. Pour les variables allouées par la pile, vous ne pouvez pas également effectuer une capture par référence, car il se peut que la variable ne soit plus 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. De cette façon, l'objet sous-jacent peut être assigné et lu, et survivra aux tâches qui l'utilisent. Utilisez cette technique même si la variable est un pointeur ou un handle contenant des références (^) vers 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 en C++.

[Premières]

Classe de tâche

Vous pouvez utiliser la classe concurrency::task pour composer les tâches en un ensemble d'opérations dépendantes. Ce modèle de composition est pris en charge par la notion des continuations. Une continuation permet au code de s'exécuter lorsque la tâche précédente, ou antécédent, se termine. Le résultat de l'antécédent est passé comme entrée en une ou plusieurs tâches de continuation. Lorsqu'une tâche antécédente est terminée, toutes les tâches de continuation en attente sont planifiées pour être exécutées. Chaque tâche de continuation reçoit une copie du résultat de l'antécédent. Ensuite, ces tâches de continuation peuvent également être des tâches antécédentes pour d'autres continuations, ce qui crée une chaîne de tâches. Les continuations vous aident à créer des chaînes de longueur arbitraire des tâches qui ont des dépendances spécifiques entre elles. En outre, 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 l'annulation de ce modèle, consultez Annulation dans la bibliothèque de modèles parallèles.

task est une classe de modèle. Le paramètre de type T est le type de résultat généré 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, d'un pointeur de fonction ou d'un objet de fonction. Pour attendre qu'une tâche se termine sans obtenir le résultat, appelez la méthode concurrency::task::wait. La méthode task::wait retourne une valeur concurrency::task_status qui indique si la tâche est 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, puis afficher sa valeur. Les exemples décrits dans cette documentation utilisent des fonctions lambda, car elles fournissent une syntaxe plus succincte. Toutefois, vous pouvez également utiliser des pointeurs de fonction et des objets de fonction lorsque 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 le mot clé auto au lieu de déclarer le type. Par exemple, utilisez le code suivant 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 à task::get ou task::wait, ou à une continuation basée sur des tâches. Pour plus d'informations sur le mécanisme de gestion des exceptions de tâches, consultez Gestion des exceptions dans le runtime d'accès concurrentiel.

Pour obtenir un exemple qui utilise task, concurrency::task_completion_event, consultez Procédure pas à pas : connexion à l'aide de tâches et de requêtes HTTP XML. (La classe task_completion_event est décrite ultérieurement dans ce document.)

Conseil

Pour apprendre les détails spécifiques aux tâches dans les applications Windows Store, consultez Asynchronous programming in C++ et Création d'opérations asynchrones en C++ pour les applications Windows Store.

[Premières]

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. Habituellement, cela se fait à l'aide 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 appelée continuation) est une tâche asynchrone appelée par une autre tâche, également appelée antécédent lorsque ce dernier est terminé. À l'aide des continuations, vous pouvez effectuer les tâches suivantes :

  • Passer des données de l'antécédent à la continuation.

  • Spécifier les conditions précises sous lesquelles la continuation doit être ou non appelée.

  • Annuler une continuation avant qu'elle ne soit lancée ou pendant son exécution de manière coopérative.

  • Fournir des conseils sur la manière de planifier la continuation. (Cette rubrique s'applique uniquement aux applications Windows Store. (Pour plus d'informations, consultez Création d'opérations asynchrones en C++ pour les applications Windows Store).

  • Appeler plusieurs continuations depuis le même antécédent.

  • Appeler une continuation lorsque certains ou tous les antécédents se terminent.

  • Chaîner des continuations les unes à la suite des autres à une longueur.

  • Utilisez 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 lorsque la première tâche se termine. Par exemple, vous pouvez créer une continuation qui compresse un fichier après que la première tâche le lit à partir du disque.

L'exemple suivant modifie l'exemple 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 les tâches de quelque longueur que ce soit. Une tâche peut également avoir plusieurs continuations. L'exemple suivant montre une chaîne de continuation de base qui incrémente trois fois la valeur de la tâche précédente.

// 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 renvoyer une autre tâche. S'il n'existe aucune annulation, cette tâche est exécutée avant la continuation suivante. Cette technique est appelée désencapsulage asynchrone. Le désencapsulage asynchrone est utile lorsque vous souhaitez effectuer du travail supplémentaire en arrière-plan, mais ne souhaitez pas que la tâche actuelle bloque le thread actuel. (C'est un phénomène courant dans les applications Windows Store, où les continuations peuvent s'exécuter sur le thread d'interface utilisateur). L'exemple suivant illustre 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

Lorsque la poursuite d'une tâche retourne une tâche imbriquée de type N, la tâche résultante a le type N, et non task<N>, et se termine lorsque la tâche imbriquée est achevée.En d'autres termes, la continuation exécute le désencapsulage de la tâche imbriquée.

[Premières]

Différences entre les continuations basées sur les valeurs et les continuations basées sur les tâches

Pour 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 qualifiée de continuation à base de valeur. Une continuation à base de valeur est planifiée pour être exécutée lorsque l'antécédent s'est exécuté sans erreur et sans avoir été annulé. Une continuation qui prend le type task<T> comme paramètre est qualifiée de continuation à base de tâche. Une continuation à base de tâche est toujours planifiée pour exécution à la fin de l'antécédent, même lorsque celui-ci est annulé ou lève une exception. Vous pouvez ensuite 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 concurrency::task_canceled. Si la tâche antécédente a levé une exception, task::get lève à nouveau cette exception. Une continuation à base de tâche n'est pas marquée comme annulée lorsque son antécédent est annulé.

[Premières]

Composition des tâches

Cette section décrit les fonctions concurrency::when_all et concurrency::when_any, qui vous permet d'utiliser plusieurs tâches pour implémenter les modèles courants.

Fonction when_all

La fonction when_all produit une tâche qui se termine quand un ensemble de tâches s'achèvent. Cette fonction retourne un objet std::vector qui contient le résultat de chaque tâche dans le jeu. 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.
*/

Notes

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 créer une tâche qui se termine après un ensemble de tâches exécutées, comme indiqué 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 après qu'un ensemble de tâches se termine. L'exemple suivant modifie l'exemple précédent pour imprimer la somme de 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 dans un ensemble de tâches est annulée ou lève une exception, when_all se termine immédiatement et n'attend pas la fin des autres tâches. Si une exception est levée, le runtime la lève à nouveau lorsque 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 choisit l'une d'entre elles. Par conséquent, assurez-vous que vous observez toutes les exceptions une fois que toutes les tâches sont terminées ; une exception de tâche non gérée arrête l'application.

Voici une fonction utilitaire que vous pouvez utiliser pour garantir que votre programme respecte toutes les exceptions. Pour chaque tâche dans la plage fournie, observe_all_exceptions déclenche une exception qui s'est produite pour être à nouveau levée, puis valide 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 Windows Store qui utilise C++ et le XAML et écrit un ensemble de fichiers sur disque. L'exemple suivant montre comment utiliser when_all et observe_all_exceptions garantir 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>
    
  2. 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);
    
  3. 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.
        });
    }
    
  4. Dans MainPage.xaml.cpp, implémentez WriteFilesAsync, comme indiqué dans l'exemple.

Conseil

when_all est une fonction non bloquante qui produit une task comme résultat.Contrairement à task::wait, il est possible d'appeler cette fonction dans une application Windows Store sur le thread ASTA (application STA).

[Premières]

Fonction when_any

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

La fonction when_any est spécialement 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 d'when_any pour traiter les résultats à mesure que chaque opération se termine. Lorsqu'une opération se termine, vous pouvez en démarrer une ou plusieurs autres.

  • 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 entre une ou plusieurs tâches et une tâche qui se termine après une heure spécifique.

Comme avec when_all, il est courant d'utiliser une continuation qui a when_any pour exécuter l'action lorsque la première tâche d'un ensemble prend fin. L'exemple de base suivant utilise when_any pour créer une tâche qui se termine lorsque 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.

Notes

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

Vous pouvez également utiliser la syntaxe || pour créer une tâche qui se termine après que la première tâche d'un ensemble de tâches s'est achevée, comme indiqué dans l'exemple suivant.

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

Conseil

Comme avec when_all, when_any est non bloquant et est suffisamment sécurisé pour appeler une application Windows Store sur le thread ASTA.

[Premières]

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

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

Les deux façons d'accomplir cette tâche sont d'utiliser une continuation ou de démarrer une tâche et d'attendre un événement dans 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 la tâche antécédente n'est pas disponible, vous pouvez créer un événement d'achèvement de tâche et le chaîner ultérieurement à la tâche antécédente lorsqu'elle devient disponible. En outre, étant donné qu'une tâche en attente bloque également un thread, vous pouvez utiliser des événements d'achèvement de tâches pour effectuer le travail lorsqu'une opération asynchrone se termine, ce qui libère un thread.

La classe concurrency::task_completion_event aide à simplifier cette composition de tâches. Comme la classe task, le paramètre de type T est le type du résultat généré 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 général, un objet task_completion_event est fourni à un thread ou à une tâche qui le signalent lorsque la valeur correspondante devient disponible. Dans le même temps, une ou plusieurs tâches sont définies comme écouteurs de cet événement. Lorsque 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 certain délai, consultez Comment : créer une tâche qui se termine après un certain délai..

[Premières]

Groupes de tâches

Un groupe de tâches organise une collection de tâches. Les groupes de tâches poussent les 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 de calcul disponibles. Après avoir ajouté des tâches à un groupe de tâches, vous pouvez attendre que toutes les tâches soient terminées ou annuler les tâches 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 exécute un travail. Comme la classe task, la fonction de travail se présente sous la forme d'une fonction lambda, d'un pointeur de fonction ou d'un objet de fonction. En général, vous n'avez pas besoin d'utiliser des objets task_handle directement. Au lieu de cela, vous passez des fonctions de travail à un groupe de tâches et le groupe de tâches crée et gère les objets task_handle.

La bibliothèque 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 PPL utilise la classe task_group pour représenter des groupes de tâches non structurés et la classe structured_task_group pour représenter des groupes de tâches structurés.

Important

La bibliothèque PPL définit également l'algorithme concurrency::parallel_invoke, qui utilise la classe structured_task_group pour exécuter un ensemble de tâches en parallèle.Étant donné que l'algorithme parallel_invoke a une syntaxe plus succincte, nous vous conseillons de l'utiliser au lieu d'utiliser la classe structured_task_group, lorsque vous le pouvez.La rubrique Algorithmes parallèles décrit parallel_invoke plus en détail.

Utilisez parallel_invoke lorsque vous voulez exécuter simultanément plusieurs tâches indépendantes et que vous devez attendre que toutes les tâches soient terminées avant de continuer. Cette technique est souvent appelée parallélisme de bifurcation/jointure. Utilisez task_group lorsque vous voulez exécuter simultanément plusieurs tâches indépendantes, mais que vous souhaitez attendre que les tâches se finissent ultérieurement. Par exemple, vous pouvez ajouter des tâches à un objet task_group et attendre qu'elles se terminent dans une autre fonction ou dans 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 souhaitez annuler l'opération globale. L'annulation empêche également le lancement des tâches qui n'ont pas encore commencé. Pour plus d'informations sur l'annulation, consultez Annulation dans la bibliothèque de modèles parallèles.

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 tout en attendant 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 dans le runtime d'accès concurrentiel.

[Premières]

Comparaison de task_group et de structured_task_group

Nous vous conseillons d'utiliser la classe task_group ou la classe parallel_invoke plutôt que la classe structured_task_group. Dans certains cas, vous pouvez cependant utiliser structured_task_group, par exemple lorsque vous écrivez un algorithme parallèle qui effectue un nombre variable de tâches ou qui nécessite 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. Vous pouvez, par conséquent, 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. Les méthodes concurrency::structured_task_group::cancel et concurrency::structured_task_group::is_canceling font exception à cette règle. 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 effectuer des tâches supplémentaires sur un objet task_group après avoir appelé la méthode concurrency::task_group::wait ou la méthode concurrency::task_group::run_and_wait. En revanche, effectuez des tâches supplémentaires sur un objet structured_task_group après avoir appelé la méthode concurrency::structured_task_group::wait ou la méthode concurrency:: structured_task_group::run_and_wait, le comportement est alors non défini.

Étant donné que la classe structured_task_group ne synchronise pas d'un thread à un autre, sa charge d'exécution est inférieure à celle de la classe task_group. Par conséquent, si votre problème ne nécessite pas la 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 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 que l'objet externe se termine. La classe task_group ne nécessite pas que les 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 les handles de tâches de différentes façons. Vous pouvez passer directement des fonctions de travail à un objet task_group. L'objet task_group pourra alors créer et gérer le handle de tâches pour vous. La classe structured_task_group nécessite la gestion d'un objet task_handle pour chaque tâche. Chaque objet task_handle doit rester valide pendant toute la durée de vie de l'objet structured_task_group associé. Utilisez la fonction concurrency::make_task pour créer un objet task_handle, 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âche lorsque le nombre de tâches est variable, utilisez une routine d'allocation de tâches, comme _malloca ou une classe de conteneur, comme std::vector.

task_group et structured_task_group prennent tous les deux en charge l'annulation. Pour plus d'informations sur l'annulation, consultez Annulation dans la bibliothèque de modèles parallèles.

[Premières]

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 une sous-tâche à 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 :

  

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

Pour obtenir des exemples complets utilisant l'algorithme parallel_invoke, consultez Comment : utiliser parallel_invoke pour écrire une routine de tri parallèle et Comment : utiliser parallel_invoke pour exécuter des opérations parallèles. Pour obtenir un exemple complet utilisant la classe task_group pour implémenter des tâches asynchrones futures, consultez Procédure pas à pas : implémentation de tâches futures.

[Premières]

Programmation fiable

Assurez-vous que vous comprenez le rôle de l'annulation et de la gestion des exceptions lorsque 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 l'exécution des tâches enfants. Cela peut entraîner des problèmes si l'une des tâches enfants effectue une opération importante pour votre application, telle que la libération d'une ressource. En outre, si une tâche enfant lève une exception, cette exception peut se propager à travers un destructeur d'objet et provoquer un comportement non défini dans votre application. Pour obtenir un exemple illustrant ces points, consultez la section Comprendre comment l'annulation et la gestion des exceptions affectent la destruction d'objet des meilleures pratiques du document Bibliothèque de modèles parallèles. Pour plus d'informations sur les modèles d'annulation et de gestion des exceptions dans la bibliothèque PPL, consultez Annulation dans la bibliothèque de modèles parallèles et Gestion des exceptions dans le runtime d'accès concurrentiel.

[Premières]

Rubriques connexes

Titre

Description

Comment : utiliser parallel_invoke pour écrire une routine de tri parallèle

Indique comment utiliser l'algorithme parallel_invoke pour améliorer les performances de l'algorithme de tri bitonique.

Comment : utiliser parallel_invoke pour exécuter des opérations parallèles

Indique 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.

Comment : créer une tâche qui se termine après un certain délai.

Indique comment utiliser les classes task, cancellation_token_source, cancellation_token et task_completion_event pour créer une tâche qui se termine après un délai.

Procédure pas à pas : implémentation de tâches futures

Indique comment combiner les fonctionnalités existantes du runtime d'accès concurrentiel afin d'en étendre et optimiser l'utilisation.

Bibliothèque de modèles parallèles

Décrit la bibliothèque PPL, qui fournit un modèle de programmation impérative pour le développement d'applications simultanées.

Référence

task (Concurrency Runtime), classe

task_completion_event, classe

when_all, fonction

when_any, fonction

task_group, classe

parallel_invoke, fonction

structured_task_group, classe