Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de changer d’annuaire.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer d’annuaire.
Dans de nombreux cas, PLINQ peut apporter des améliorations significatives des performances sur les requêtes LINQ to Objects séquentielles. Toutefois, le travail de parallélisation de l’exécution de la requête introduit une complexité qui peut entraîner des problèmes qui, dans le code séquentiel, ne sont pas aussi courants ou ne sont pas du tout rencontrés. Cette rubrique répertorie certaines pratiques à éviter lorsque vous écrivez des requêtes PLINQ.
Ne partez pas du principe que le parallèle est toujours plus rapide
La parallélisation entraîne parfois l’exécution d’une requête PLINQ plus lente que son équivalent LINQ to Objects. La règle empirique de base veut que les requêtes ayant peu d’éléments source et des délégués utilisateurs rapides ne sont pas susceptibles d’apporter une grande accélération. Toutefois, étant donné que de nombreux facteurs sont impliqués dans les performances, nous vous recommandons de mesurer les résultats réels avant de décider s’il faut utiliser PLINQ. Pour plus d’informations, consultez Comprendre l'accélération dans PLINQ.
Évitez d’écrire dans des emplacements de mémoire partagée
Dans le code séquentiel, il n’est pas rare de lire ou d’écrire dans des variables statiques ou des champs de classe. Toutefois, l’accès simultané de plusieurs threads à de telles variables entraîne un fort risque d’engorgement. Même si vous pouvez utiliser des verrous pour synchroniser l’accès à la variable, le coût de la synchronisation peut nuire aux performances. Par conséquent, nous vous recommandons d’éviter, ou au moins de limiter, l’accès à l’état partagé dans une requête PLINQ autant que possible.
Éviter une surallélisation
En utilisant la méthode AsParallel, vous engagez une surcharge due au partitionnement de la collection source et à la synchronisation des threads de travail. Les avantages de la parallélisation sont encore limités par le nombre de processeurs sur l’ordinateur. Il n’y a pas de vitesse à gagner en exécutant plusieurs threads liés au calcul sur un seul processeur. Par conséquent, vous devez veiller à ne pas surparalléliser une requête.
Le scénario le plus courant dans lequel la surallélisation peut se produire se trouve dans les requêtes imbriquées, comme illustré dans l’extrait de code suivant.
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };
Dim q = From cust In customers.AsParallel()
From order In cust.Orders.AsParallel()
Where order.OrderDate > aDate
Select New With {cust, order}
Dans ce cas, il est préférable de paralléliser uniquement la source de données externe (clients), sauf si une ou plusieurs des conditions suivantes s’appliquent :
La source de données interne (cust.Orders) est connue pour être très longue.
Vous effectuez un calcul coûteux sur chaque commande. (L’opération illustrée dans l’exemple n’est pas coûteuse.)
Le système cible est connu pour avoir suffisamment de processeurs pour gérer le nombre de threads qui seront générés en parallélisant la requête sur
cust.Orders.
Dans tous les cas, la meilleure façon de déterminer la forme de requête optimale consiste à tester et mesurer. Pour plus d’informations, consultez Guide pratique pour mesurer les performances des requêtes PLINQ.
Éviter de faire appel aux méthodes qui ne sont pas thread-safe
L’écriture dans des méthodes d’instance qui ne sont pas thread-safe à partir d’une requête PLINQ peut entraîner une corruption des données qui peut être détectée ou non dans votre programme. Elle peut également entraîner des exceptions. Dans l’exemple suivant, plusieurs threads tentent d’appeler la FileStream.Write méthode simultanément, ce qui n’est pas pris en charge par la classe.
Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));
Limiter les appels aux méthodes qui sont thread-safe
La plupart des méthodes statiques dans .NET sont thread-safe et peuvent être appelées simultanément à partir de plusieurs threads. Toutefois, même dans ces cas, la synchronisation impliquée peut entraîner un ralentissement significatif de la requête.
Remarque
Vous pouvez tester cela vous-même en insérant des appels à WriteLine dans vos requêtes. Bien que cette méthode soit utilisée dans les exemples de documentation à des fins de démonstration, ne l’utilisez pas dans les requêtes PLINQ.
Éviter les opérations de classement inutiles
Lorsque PLINQ exécute une requête en parallèle, elle divise la séquence source en partitions pouvant être exploitées simultanément sur plusieurs threads. Par défaut, l’ordre dans lequel les partitions sont traitées et les résultats sont remis n’est pas prévisible (sauf pour les opérateurs tels que OrderBy). Vous pouvez demander à PLINQ de conserver l’ordre de n’importe quelle séquence source, mais cela a un impact négatif sur les performances. La meilleure pratique, dans la mesure du possible, consiste à structurer les requêtes afin qu’elles ne s’appuient pas sur la préservation de l’ordre. Pour plus d’informations, consultez La préservation de l’ordre dans PLINQ.
Préférer ForAll à ForEach quand il est possible
Bien que PLINQ exécute une requête sur plusieurs threads, si vous consommez les résultats d’une foreach boucle (For Each en Visual Basic), les résultats de la requête doivent être fusionnés dans un thread et accessibles en série par l’énumérateur. Dans certains cas, cela est inévitable ; Toutefois, dans la mesure du possible, utilisez la ForAll méthode pour permettre à chaque thread de générer ses propres résultats, par exemple, en écrivant dans une collection thread-safe telle que System.Collections.Concurrent.ConcurrentBag<T>.
Le même problème s’applique à Parallel.ForEach. En d’autres termes, source.AsParallel().Where().ForAll(...) devrait être fortement préféré à Parallel.ForEach(source.AsParallel().Where(), ...).
Tenir compte des problèmes d’affinité de thread
Certaines technologies, par exemple, l’interopérabilité COM pour les composants Single-Threaded Apartment (STA), Windows Forms et Windows Presentation Foundation (WPF), imposent des restrictions d’affinité de thread qui nécessitent l’exécution du code sur un thread spécifique. Par exemple, dans Windows Forms et WPF, un contrôle est accessible uniquement sur le thread sur lequel il a été créé. Si vous essayez d’accéder à l’état partagé d’un contrôle Windows Forms dans une requête PLINQ, une exception est générée pendant l’exécution dans le débogueur. (Ce paramètre peut être désactivé.) Toutefois, si votre requête est consommée sur le thread d’interface utilisateur, vous pouvez accéder au contrôle à partir de la boucle qui énumère les résultats de la foreach requête, car ce code s’exécute sur un seul thread.
Ne supposez pas que les itérations de ForEach, For et ForAll s’exécutent toujours en parallèle
Il est important de garder à l'esprit que les itérations individuelles d'une boucle Parallel.For, Parallel.ForEach, ou ForAll peuvent, mais n'ont pas besoin, être exécutées en parallèle. Par conséquent, vous devez éviter d’écrire du code qui dépend de l’exactitude de l’exécution parallèle d’itérations ou de l’exécution d’itérations dans un ordre particulier.
Par exemple, ce code est susceptible d’interblocage :
Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
If j = Environment.ProcessorCount Then
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Wait()
End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
Dans cet exemple, une itération définit un événement et toutes les autres itérations attendent l’événement. Aucune des itérations en attente ne peut s’achever tant que l’itération de définition d’événement n’est pas terminée. Toutefois, il est possible que les itérations en attente bloquent tous les threads utilisés pour exécuter la boucle parallèle, avant l’exécution de l’itération de paramètre d’événement. Cela entraîne un blocage : l’itération de paramètre d’événement ne s’exécutera jamais et les itérations en attente ne se réveilleront jamais.
En particulier, une itération d’une boucle parallèle ne doit jamais attendre une autre itération de la boucle pour progresser. Si la boucle parallèle décide de planifier les itérations de manière séquentielle, mais dans l’ordre opposé, un interblocage se produit.