Meilleures pratiques de la Bibliothèque de modèles parallèles
Ce document décrit la meilleure façon d’utiliser efficacement la bibliothèque de modèles parallèles (PPL). La bibliothèque de modèles parallèles (PPL) fournit des conteneurs, des objets et des algorithmes à usage général pour effectuer un parallélisme affiné.
Pour plus d’informations sur la bibliothèque PPL, consultez Bibliothèque de modèles parallèles (PPL).
Sections
Ce document contient les sections suivantes :
Utiliser parallel_invoke pour résoudre les problèmes de division et de conquête
Utiliser la gestion des annulations ou des exceptions pour arrêter une boucle parallèle
Comprendre comment l’annulation et la gestion des exceptions affectent la destruction d’objets
Ne pas bloquer à plusieurs reprises dans une boucle parallèle
N’effectuez pas d’opérations bloquantes lorsque vous annulez le travail parallèle
Ne pas écrire dans des données partagées dans une boucle parallèle
Vérifiez que les variables sont valides pendant toute la durée de vie d’une tâche
Ne pas paralléliser les corps de petites boucles
La parallélisation des corps de boucles relativement petites peut entraîner une surcharge de la planification associée pour compenser les avantages du traitement parallèle. Prenons l'exemple suivant, qui ajoute chaque paire d'éléments dans deux tableaux.
// small-loops.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Create three arrays that each have the same size.
const size_t size = 100000;
int a[size], b[size], c[size];
// Initialize the arrays a and b.
for (size_t i = 0; i < size; ++i)
{
a[i] = i;
b[i] = i * 2;
}
// Add each pair of elements in arrays a and b in parallel
// and store the result in array c.
parallel_for<size_t>(0, size, [&a,&b,&c](size_t i) {
c[i] = a[i] + b[i];
});
// TODO: Do something with array c.
}
La charge de travail de chaque itération de boucle parallèle est trop petite pour bénéficier de la surcharge de traitement parallèle. Vous pouvez améliorer les performances de cette boucle en effectuant plus de travail dans le corps de la boucle ou en exécutant la boucle en série.
[Haut]
Parallélisme express au niveau le plus élevé possible
Quand vous parallélisez du code uniquement au niveau inférieur, vous pouvez introduire une construction de bifurcation-jointure qui n'évolue pas au fur et à mesure que le nombre de processeurs augmente. Une construction de jointure de fourche est une construction où une tâche divise son travail en sous-tâches parallèles plus petites et attend que ces sous-tâches se terminent. Chaque sous-tâche peut elle-même se diviser de manière récursive en sous-tâches supplémentaires.
Bien que le modèle de bifurcation-jointure puisse s’avérer utile pour résoudre de nombreux problèmes, dans certaines situations, la surcharge de synchronisation peut réduire l’évolutivité. Par exemple, considérez le code en série suivant qui traite des données d'image.
// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
int width = bmp->GetWidth();
int height = bmp->GetHeight();
// Lock the bitmap.
BitmapData bitmapData;
Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);
// Get a pointer to the bitmap data.
DWORD* image_bits = (DWORD*)bitmapData.Scan0;
// Call the function for each pixel in the image.
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
// Get the current pixel value.
DWORD* curr_pixel = image_bits + (y * width) + x;
// Call the function.
f(*curr_pixel);
}
}
// Unlock the bitmap.
bmp->UnlockBits(&bitmapData);
}
Étant donné que chaque itération de boucle est indépendante, vous pouvez paralléliser une grande partie du travail, comme indiqué dans l'exemple suivant. Cet exemple utilise l’algorithme concurrency ::p arallel_for pour paralléliser la boucle externe.
// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
int width = bmp->GetWidth();
int height = bmp->GetHeight();
// Lock the bitmap.
BitmapData bitmapData;
Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);
// Get a pointer to the bitmap data.
DWORD* image_bits = (DWORD*)bitmapData.Scan0;
// Call the function for each pixel in the image.
parallel_for (0, height, [&, width](int y)
{
for (int x = 0; x < width; ++x)
{
// Get the current pixel value.
DWORD* curr_pixel = image_bits + (y * width) + x;
// Call the function.
f(*curr_pixel);
}
});
// Unlock the bitmap.
bmp->UnlockBits(&bitmapData);
}
L'exemple suivant illustre une construction de bifurcation-jointure en appelant la fonction ProcessImage
dans une boucle. Chaque appel à ProcessImage
ne retourne rien tant que chaque sous-tâche n'est pas terminée.
// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
for_each(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
ProcessImage(bmp, f);
});
}
Si chaque itération de la boucle parallèle n'exécute quasiment aucun travail ou si le travail effectué par la boucle parallèle est déséquilibré, c'est-à-dire que certaines itérations de boucle prennent plus de temps que d'autres, la surcharge de planification requise pour la bifurcation et la jointure fréquentes du travail peut l'emporter sur l'avantage d'une exécution parallèle. Cette surcharge augmente à mesure que le nombre de processeurs augmente.
Pour réduire la quantité de surcharge de planification dans cet exemple, vous pouvez paralléliser les boucles externes avant de paralléliser les boucles internes ou utiliser une autre construction parallèle comme le traitement « pipeline ». L’exemple suivant modifie la ProcessImages
fonction pour utiliser l’algorithme concurrency ::p arallel_for_each pour paralléliser la boucle externe.
// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
parallel_for_each(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
ProcessImage(bmp, f);
});
}
Pour obtenir un exemple similaire qui utilise un pipeline pour effectuer le traitement d’images en parallèle, consultez Procédure pas à pas : Création d’un réseau de traitement d’images.
[Haut]
Utiliser parallel_invoke pour résoudre les problèmes de division et de conquête
Un problème de division et de conquête est une forme de construction de jointure de fourche qui utilise la récursivité pour briser une tâche en tâches subordonnées. Outre les classes concurrency ::task_group et concurrency ::structured_task_group , vous pouvez également utiliser l’algorithme concurrency ::p arallel_invoke pour résoudre les problèmes de division et de conquête. L’algorithme parallel_invoke
a une syntaxe plus succincte que les objets de groupes de tâches. Il s’avère utile quand vous avez un nombre fixe de tâches parallèles.
L'exemple suivant illustre l'utilisation de l'algorithme parallel_invoke
pour implémenter l'algorithme de tri bitonique.
// Sorts the given sequence in the specified order.
template <class T>
void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
{
if (n > 1)
{
// Divide the array into two partitions and then sort
// the partitions in different directions.
int m = n / 2;
parallel_invoke(
[&] { parallel_bitonic_sort(items, lo, m, INCREASING); },
[&] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
);
// Merge the results.
parallel_bitonic_merge(items, lo, n, dir);
}
}
Pour réduire la surcharge, l’algorithme parallel_invoke
effectue la dernière série de tâches sur le contexte d’appel.
Pour obtenir la version complète de cet exemple, consultez How to : Use parallel_invoke to Write a Parallel Sort Routine. Pour plus d’informations sur l’algorithme parallel_invoke
, consultez Algorithmes parallèles.
[Haut]
Utiliser la gestion des annulations ou des exceptions pour arrêter une boucle parallèle
La bibliothèque de modèles parallèles fournit deux méthodes pour annuler un travail parallèle exécuté par un groupe de tâches ou un algorithme parallèle. L’une des méthodes consiste à utiliser le mécanisme d’annulation fourni par les classes concurrency ::task_group et concurrency ::structured_task_group . La seconde consiste à lever une exception dans le corps d'une fonction de travail de tâche. Le mécanisme d’annulation est plus efficace que la gestion des exceptions pour annuler une arborescence de travail parallèle. Une arborescence de travail parallèle est un groupe de groupes de tâches associés dans lequel certains groupes de tâches contiennent d’autres groupes de tâches. Le mécanisme d'annulation annule un groupe de tâches et ses groupes de tâches enfants de haut en bas. À l’inverse, la gestion des exceptions fonctionne de bas en haut et doit annuler chaque groupe de tâches enfant indépendamment puisque l’exception se propage vers le haut.
Lorsque vous travaillez directement avec un objet de groupe de tâches, utilisez les méthodes concurrency ::task_group ::cancel ou concurrency ::structured_task_group ::cancel pour annuler le travail qui appartient à ce groupe de tâches. Pour annuler un algorithme parallèle, par exemple, parallel_for
, créez un groupe de tâches parent et annulez ce groupe de tâches. Par exemple, considérez la fonction suivante, parallel_find_any
, qui recherche une valeur dans un tableau en parallèle.
// Returns the position in the provided array that contains the given value,
// or -1 if the value is not in the array.
template<typename T>
int parallel_find_any(const T a[], size_t count, const T& what)
{
// The position of the element in the array.
// The default value, -1, indicates that the element is not in the array.
int position = -1;
// Call parallel_for in the context of a cancellation token to search for the element.
cancellation_token_source cts;
run_with_cancellation_token([count, what, &a, &position, &cts]()
{
parallel_for(std::size_t(0), count, [what, &a, &position, &cts](int n) {
if (a[n] == what)
{
// Set the return value and cancel the remaining tasks.
position = n;
cts.cancel();
}
});
}, cts.get_token());
return position;
}
Étant donné que les algorithmes parallèles utilisent des groupes de tâches, quand l'une des itérations parallèles annule le groupe de tâches parent, la tâche globale est annulée. Pour obtenir la version complète de cet exemple, consultez How to : Use Cancellation to Break from a Parallel Loop.
Bien que la gestion des exceptions soit moins efficace pour annuler un travail parallèle que le mécanisme d'annulation, dans certains cas, elle est appropriée. Par exemple, la méthode suivante, for_all
, exécute une fonction de travail de manière récursive sur chaque nœud d'une structure tree
. Dans cet exemple, le _children
membre de données est un std ::list qui contient des tree
objets.
// Performs the given work function on the data element of the tree and
// on each child.
template<class Function>
void tree::for_all(Function& action)
{
// Perform the action on each child.
parallel_for_each(begin(_children), end(_children), [&](tree& child) {
child.for_all(action);
});
// Perform the action on this node.
action(*this);
}
L’appelant de la méthode tree::for_all
peut lever une exception s’il n’a pas besoin que la fonction de travail soit appelée sur chaque élément de l’arborescence. L'exemple suivant illustre la fonction search_for_value
, qui recherche une valeur dans l'objet tree
fourni. La fonction search_for_value
utilise une fonction de travail qui lève une exception quand l’élément actuel de l’arborescence correspond à la valeur fournie. La fonction search_for_value
utilise un bloc try-catch
pour capturer l'exception et imprimer le résultat dans la console.
// Searches for a value in the provided tree object.
template <typename T>
void search_for_value(tree<T>& t, int value)
{
try
{
// Call the for_all method to search for a value. The work function
// throws an exception when it finds the value.
t.for_all([value](const tree<T>& node) {
if (node.get_data() == value)
{
throw &node;
}
});
}
catch (const tree<T>* node)
{
// A matching node was found. Print a message to the console.
wstringstream ss;
ss << L"Found a node with value " << value << L'.' << endl;
wcout << ss.str();
return;
}
// A matching node was not found. Print a message to the console.
wstringstream ss;
ss << L"Did not find node with value " << value << L'.' << endl;
wcout << ss.str();
}
Pour obtenir la version complète de cet exemple, consultez How to : Use Exception Handling to Break from a Parallel Loop.
Pour plus d’informations générales sur les mécanismes d’annulation et de gestion des exceptions fournis par le PPL, consultez Annulation dans ppL et gestion des exceptions.
[Haut]
Comprendre comment l’annulation et la gestion des exceptions affectent la destruction d’objets
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, l’annulation d’une tâche peut déclencher la propagation d’une exception via un destructeur d’objet et entraîner un comportement non défini dans votre application.
Dans l'exemple suivant, la classe Resource
décrit une ressource et la classe Container
décrit un conteneur qui contient des ressources. Dans son destructeur, la classe Container
appelle la méthode cleanup
sur deux de ses membres Resource
en parallèle, puis appelle la méthode cleanup
sur son troisième membre Resource
.
// parallel-resource-destruction.h
#pragma once
#include <ppl.h>
#include <sstream>
#include <iostream>
// Represents a resource.
class Resource
{
public:
Resource(const std::wstring& name)
: _name(name)
{
}
// Frees the resource.
void cleanup()
{
// Print a message as a placeholder.
std::wstringstream ss;
ss << _name << L": Freeing..." << std::endl;
std::wcout << ss.str();
}
private:
// The name of the resource.
std::wstring _name;
};
// Represents a container that holds resources.
class Container
{
public:
Container(const std::wstring& name)
: _name(name)
, _resource1(L"Resource 1")
, _resource2(L"Resource 2")
, _resource3(L"Resource 3")
{
}
~Container()
{
std::wstringstream ss;
ss << _name << L": Freeing resources..." << std::endl;
std::wcout << ss.str();
// For illustration, assume that cleanup for _resource1
// and _resource2 can happen concurrently, and that
// _resource3 must be freed after _resource1 and _resource2.
concurrency::parallel_invoke(
[this]() { _resource1.cleanup(); },
[this]() { _resource2.cleanup(); }
);
_resource3.cleanup();
}
private:
// The name of the container.
std::wstring _name;
// Resources.
Resource _resource1;
Resource _resource2;
Resource _resource3;
};
Bien que ce modèle ne présente aucun problème particulier, considérez le code suivant qui exécute deux tâches en parallèle. La première tâche crée un objet Container
et la seconde annule la tâche globale. Pour illustration, l’exemple utilise deux objets concurrency ::event pour s’assurer que l’annulation se produit après la création de l’objet Container
et que l’objet Container
est détruit après l’opération d’annulation.
// parallel-resource-destruction.cpp
// compile with: /EHsc
#include "parallel-resource-destruction.h"
using namespace concurrency;
using namespace std;
static_assert(false, "This example illustrates a non-recommended practice.");
int main()
{
// Create a task_group that will run two tasks.
task_group tasks;
// Used to synchronize the tasks.
event e1, e2;
// Run two tasks. The first task creates a Container object. The second task
// cancels the overall task group. To illustrate the scenario where a child
// task is not run because its parent task is cancelled, the event objects
// ensure that the Container object is created before the overall task is
// cancelled and that the Container object is destroyed after the overall
// task is cancelled.
tasks.run([&tasks,&e1,&e2] {
// Create a Container object.
Container c(L"Container 1");
// Allow the second task to continue.
e2.set();
// Wait for the task to be cancelled.
e1.wait();
});
tasks.run([&tasks,&e1,&e2] {
// Wait for the first task to create the Container object.
e2.wait();
// Cancel the overall task.
tasks.cancel();
// Allow the first task to continue.
e1.set();
});
// Wait for the tasks to complete.
tasks.wait();
wcout << L"Exiting program..." << endl;
}
Cet exemple produit la sortie suivante :
Container 1: Freeing resources...Exiting program...
Cet exemple de code contient les problèmes suivants susceptibles d'entraîner un comportement différent de celui que vous attendez :
L’annulation de la tâche parente entraîne l’annulation de la tâche enfant, l’appel à concurrency ::p arallel_invoke, doit également être annulé. Ainsi, ces deux ressources ne sont pas libérées.
L’annulation de la tâche parente amène la tâche enfant à lever une exception interne. Étant donné que le destructeur
Container
ne gère pas cette exception, l'exception est propagée vers le haut et la troisième ressource n'est pas libérée.L’exception qui est levée par la tâche enfant se propage via le destructeur
Container
. La levée à partir d'un destructeur met l'application dans un état non défini.
Nous vous recommandons de ne pas effectuer d'opérations critiques, comme la libération de ressources, dans des tâches, sauf si vous pouvez garantir que ces tâches ne seront pas annulées. Nous vous recommandons également de ne pas utiliser de fonctionnalités runtime pouvant se déclencher dans le destructeur de vos types.
[Haut]
Ne pas bloquer à plusieurs reprises dans une boucle parallèle
Une boucle parallèle telle que concurrency ::p arallel_for ou concurrency ::p arallel_for_each qui est dominée par les opérations de blocage peut entraîner la création de nombreux threads sur une courte durée.
Le runtime d'accès concurrentiel effectue un travail supplémentaire quand une tâche se termine, se bloque ou génère un résultat de manière coopérative. Quand une itération de boucle parallèle se bloque, le runtime peut démarrer une autre itération. Quand il n'existe aucun thread inactif disponible, le runtime crée un thread.
Quand le corps d'une boucle parallèle se bloque de temps en temps, ce mécanisme permet d'optimiser le débit global de la tâche. Toutefois, quand de nombreuses itérations se bloquent, le runtime peut créer de nombreux threads pour exécuter le travail supplémentaire. Cela peut entraîner des conditions de mémoire insuffisante ou une mauvaise utilisation des ressources matérielles.
Prenons l’exemple suivant qui appelle la fonction concurrency ::send dans chaque itération d’une parallel_for
boucle. Étant donné que send
se bloque de manière coopérative, le runtime crée un thread pour exécuter le travail supplémentaire chaque fois que send
est appelé.
// repeated-blocking.cpp
// compile with: /EHsc
#include <ppl.h>
#include <agents.h>
using namespace concurrency;
static_assert(false, "This example illustrates a non-recommended practice.");
int main()
{
// Create a message buffer.
overwrite_buffer<int> buffer;
// Repeatedly send data to the buffer in a parallel loop.
parallel_for(0, 1000, [&buffer](int i) {
// The send function blocks cooperatively.
// We discourage the use of repeated blocking in a parallel
// loop because it can cause the runtime to create
// a large number of threads over a short period of time.
send(buffer, i);
});
}
Nous vous recommandons de refactoriser votre code pour éviter ce modèle. Dans cet exemple, vous pouvez éviter la création de threads supplémentaires en appelant send
dans une boucle for
en série.
[Haut]
N’effectuez pas d’opérations bloquantes lorsque vous annulez le travail parallèle
Si possible, n’effectuez pas d’opérations bloquantes avant d’appeler la méthode concurrency ::task_group ::cancel ou concurrency ::structured_task_group ::cancel pour annuler le travail parallèle.
Quand une tâche effectue une opération de blocage coopérative, le runtime peut effectuer un autre travail pendant que la première tâche attend des données. Le runtime replanifie la tâche qui attend quand elle se débloque. En général, le runtime replanifie les dernières tâches qui ont été débloquées avant de replanifier celles qui l’ont été auparavant. Ainsi, le runtime peut planifier du travail inutile lors de l'opération de blocage, ce qui entraîne une diminution des performances. En conséquence, quand vous effectuez une opération de blocage avant d'annuler un travail parallèle, celle-ci peut retarder l'appel à cancel
. Cette opération amène d'autres tâches à effectuer du travail inutile.
Prenons l'exemple suivant qui définit la fonction parallel_find_answer
, qui recherche un élément du tableau fourni qui satisfait à la fonction de prédicat fournie. Lorsque la fonction de prédicat retourne true
, la fonction de travail parallèle crée un Answer
objet et annule la tâche globale.
// blocking-cancel.cpp
// compile with: /c /EHsc
#include <windows.h>
#include <ppl.h>
using namespace concurrency;
// Encapsulates the result of a search operation.
template<typename T>
class Answer
{
public:
explicit Answer(const T& data)
: _data(data)
{
}
T get_data() const
{
return _data;
}
// TODO: Add other methods as needed.
private:
T _data;
// TODO: Add other data members as needed.
};
// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
// The result of the search.
Answer<T>* answer = nullptr;
// Ensures that only one task produces an answer.
volatile long first_result = 0;
// Use parallel_for and a task group to search for the element.
structured_task_group tasks;
tasks.run_and_wait([&]
{
// Declare the type alias for use in the inner lambda function.
typedef T T;
parallel_for<size_t>(0, count, [&](const T& n) {
if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
{
// Create an object that holds the answer.
answer = new Answer<T>(a[n]);
// Cancel the overall task.
tasks.cancel();
}
});
});
return answer;
}
L'opérateur new
effectue une allocation de tas, qui peut se bloquer. Le runtime effectue d’autres tâches uniquement lorsque la tâche effectue un appel de blocage coopératif, tel qu’un appel à concurrency ::critical_section ::lock.
L'exemple suivant montre comment empêcher le travail inutile et ainsi améliorer les performances. Cet exemple annule le groupe de tâches avant qu’il alloue le stockage pour l’objet Answer
.
// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
// The result of the search.
Answer<T>* answer = nullptr;
// Ensures that only one task produces an answer.
volatile long first_result = 0;
// Use parallel_for and a task group to search for the element.
structured_task_group tasks;
tasks.run_and_wait([&]
{
// Declare the type alias for use in the inner lambda function.
typedef T T;
parallel_for<size_t>(0, count, [&](const T& n) {
if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
{
// Cancel the overall task.
tasks.cancel();
// Create an object that holds the answer.
answer = new Answer<T>(a[n]);
}
});
});
return answer;
}
[Haut]
Ne pas écrire dans des données partagées dans une boucle parallèle
Le runtime d’accès concurrentiel fournit plusieurs structures de données, par exemple, concurrency ::critical_section, qui synchronisent l’accès simultané aux données partagées. Ces structures de données s'avèrent utiles dans de nombreux cas, par exemple, quand plusieurs tâches requièrent peu souvent un accès partagé à une ressource.
Prenons l’exemple suivant qui utilise l’algorithme concurrency ::p arallel_for_each et un critical_section
objet pour calculer le nombre de nombres premiers dans un objet std ::array . Cet exemple n'évolue pas car chaque thread doit attendre d'accéder à la variable partagée prime_sum
.
critical_section cs;
prime_sum = 0;
parallel_for_each(begin(a), end(a), [&](int i) {
cs.lock();
prime_sum += (is_prime(i) ? i : 0);
cs.unlock();
});
Cet exemple peut également entraîner une baisse des performances car l'opération de verrouillage fréquente sérialise efficacement la boucle. De plus, quand un objet de runtime d'accès concurrentiel effectue une opération de blocage, le planificateur peut créer un thread supplémentaire pour effectuer un autre travail pendant que le premier thread attend des données. Si le runtime crée de nombreux threads car de nombreuses tâches attendent des données partagées, l'application peut fonctionner difficilement ou entrer dans un état de ressources insuffisantes.
Le PPL définit la classe concurrency ::combinable , qui vous permet d’éliminer l’état partagé en fournissant l’accès aux ressources partagées de manière sans verrou. La classe combinable
fournit un stockage local des threads qui vous permet d’effectuer des calculs affinés, puis de fusionner ces calculs dans un résultat final. Vous pouvez considérer un objet combinable
comme une variable de réduction.
L'exemple suivant modifie le précédent en utilisant un objet combinable
au lieu d'un objet critical_section
pour calculer la somme. Cet exemple évolue car chaque thread retient sa propre copie locale de la somme. Cet exemple utilise la méthode concurrency ::combinable ::combine pour fusionner les calculs locaux dans le résultat final.
combinable<int> sum;
parallel_for_each(begin(a), end(a), [&](int i) {
sum.local() += (is_prime(i) ? i : 0);
});
prime_sum = sum.combine(plus<int>());
Pour obtenir la version complète de cet exemple, consultez Guide pratique pour améliorer les performances. Pour plus d’informations sur la combinable
classe, consultez Conteneurs et objets parallèles.
[Haut]
Si possible, évitez le partage faux
Le partage faux se produit lorsque plusieurs tâches simultanées qui s’exécutent sur des processeurs distincts écrivent dans des variables situées sur la même ligne de cache. Quand une seule tâche écrit dans l’une des variables, la ligne de cache des deux variables est invalidée. Chaque processeur doit recharger la ligne de cache à chaque fois que la ligne de cache est invalidée. Ainsi, le faux partage peut diminuer les performances dans votre application.
L’exemple de base suivant illustre deux tâches simultanées qui incrémente chacune une variable de compteur partagée.
volatile long count = 0L;
concurrency::parallel_invoke(
[&count] {
for(int i = 0; i < 100000000; ++i)
InterlockedIncrement(&count);
},
[&count] {
for(int i = 0; i < 100000000; ++i)
InterlockedIncrement(&count);
}
);
Pour éliminer le partage des données entre les deux tâches, vous pouvez modifier l’exemple pour utiliser deux variables de compteur. Cet exemple calcule la valeur de compteur finale une fois que les tâches sont terminées. Toutefois, cet exemple illustre le faux partage car les variables count1
et count2
sont susceptibles de se trouver sur la même ligne de cache.
long count1 = 0L;
long count2 = 0L;
concurrency::parallel_invoke(
[&count1] {
for(int i = 0; i < 100000000; ++i)
++count1;
},
[&count2] {
for(int i = 0; i < 100000000; ++i)
++count2;
}
);
long count = count1 + count2;
Un moyen de supprimer le faux partage consiste à s'assurer que les variables de compteur sont sur des lignes de cache distinctes. L'exemple suivant aligne les variables count1
et count2
sur des limites de 64 octets.
__declspec(align(64)) long count1 = 0L;
__declspec(align(64)) long count2 = 0L;
concurrency::parallel_invoke(
[&count1] {
for(int i = 0; i < 100000000; ++i)
++count1;
},
[&count2] {
for(int i = 0; i < 100000000; ++i)
++count2;
}
);
long count = count1 + count2;
Cet exemple suppose que la taille du cache en mémoire s'élève à 64 octets ou moins.
Nous vous recommandons d’utiliser la classe concurrency ::combinable lorsque vous devez partager des données entre les tâches. La classe combinable
crée des variables locales des thread de manière à ce que le faux partage soit moins probable. Pour plus d’informations sur la combinable
classe, consultez Conteneurs et objets parallèles.
[Haut]
Vérifiez que les variables sont valides pendant toute la durée de vie d’une tâche
Quand vous fournissez une expression lambda à un groupe de tâches ou à un algorithme parallèle, la clause de capture spécifie si le corps de l’expression lambda accède aux variables dans la portée englobante par valeur ou par référence. 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.
Prenons l'exemple suivant qui définit la classe object
et la fonction perform_action
. La fonction perform_action
crée une variable object
et effectue une action sur cette variable de façon asynchrone. Étant donné que la fin de la tâche n'est pas garantie avant le retour de la fonction perform_action
, le programme se bloque ou adopte un comportement non spécifié si la variable object
est détruite quand la tâche est en cours d'exécution.
// lambda-lifetime.cpp
// compile with: /c /EHsc
#include <ppl.h>
using namespace concurrency;
// A type that performs an action.
class object
{
public:
void action() const
{
// TODO: Details omitted for brevity.
}
};
// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
// Create an object variable and perform some action on
// that variable asynchronously.
object obj;
tasks.run([&obj] {
obj.action();
});
// NOTE: The object variable is destroyed here. The program
// will crash or exhibit unspecified behavior if the task
// is still running when this function returns.
}
Selon les besoins de votre application, vous pouvez utiliser une des techniques suivantes pour garantir la validité des variables pendant toute la durée de chaque tâche.
L'exemple suivant passe la variable object
par valeur à la tâche. Par conséquent, la tâche s’exécute sur sa propre copie de la variable.
// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
// Create an object variable and perform some action on
// that variable asynchronously.
object obj;
tasks.run([obj] {
obj.action();
});
}
Étant donné que la variable object
est passée par valeur, toute modification d'état qui se produit pour cette variable n'apparaît pas dans la copie d'origine.
L’exemple suivant utilise la méthode concurrency ::task_group ::wait pour vous assurer que la tâche se termine avant que la perform_action
fonction ne retourne.
// Performs an action.
void perform_action(task_group& tasks)
{
// Create an object variable and perform some action on
// that variable.
object obj;
tasks.run([&obj] {
obj.action();
});
// Wait for the task to finish.
tasks.wait();
}
Étant donné que la tâche se termine désormais avant le retour de la fonction, la fonction perform_action
ne se comporte plus de façon asynchrone.
L'exemple suivant modifie la fonction perform_action
pour prendre une référence à la variable object
. L’appelant doit garantir que la durée de vie de la variable object
est valide jusqu’à ce que la tâche se termine.
// Performs an action asynchronously.
void perform_action(object& obj, task_group& tasks)
{
// Perform some action on the object variable.
tasks.run([&obj] {
obj.action();
});
}
Vous pouvez également utiliser un pointeur pour contrôler la durée de vie d'un objet que vous passez à un groupe de tâches ou à un algorithme parallèle.
Pour plus d’informations sur les expressions lambda, consultez Expressions Lambda.
[Haut]
Voir aussi
Bonnes pratiques sur le runtime d’accès concurrentiel
Bibliothèque de modèles parallèles
Conteneurs et objets parallèles
Algorithmes parallèles
Annulation dans la bibliothèque de modèles parallèles
Gestion des exceptions
Procédure pas à pas : création d’un réseau de traitement d’image
Guide pratique pour utiliser parallel_invoke pour écrire une routine de tri parallèle
Guide pratique pour utiliser l’annulation pour rompre une boucle parallèle
Guide pratique pour utiliser la classe combinable pour améliorer les performances
Bonnes pratiques pour la bibliothèque d’agents asynchrones
Bonnes pratiques en général du runtime d’accès concurrentiel