Pièges potentiels dans le parallélisme des données et des tâchesPotential Pitfalls in Data and Task Parallelism

Dans de nombreux cas, Parallel.For et Parallel.ForEach permettent une amélioration significative des performances par rapport à des boucles séquentielles ordinaires.In many cases, Parallel.For and Parallel.ForEach can provide significant performance improvements over ordinary sequential loops. Toutefois, le travail de la parallélisation de la boucle présente une certaine complexité pouvant entraîner des problèmes qui, dans du code séquentiel, ne sont pas si courants ou ne surviennent pas du tout.However, the work of parallelizing the loop introduces complexity that can lead to problems that, in sequential code, are not as common or are not encountered at all. Cette rubrique répertorie les pratiques à éviter lorsque vous écrivez des boucles parallèles.This topic lists some practices to avoid when you write parallel loops.

Ne partez pas du principe qu’une boucle parallèle est toujours plus rapideDo Not Assume That Parallel Is Always Faster

Dans certains cas, elle peut s’exécuter plus lentement qu’une boucle séquentielle équivalente.In certain cases a parallel loop might run slower than its sequential equivalent. La règle empirique de base veut que les boucles parallèles ayant peu d’itérations et des délégués utilisateurs rapides ne soient pas susceptibles d’apporter une grande accélération.The basic rule of thumb is that parallel loops that have few iterations and fast user delegates are unlikely to speedup much. Toutefois, étant donné que de nombreux facteurs sont impliqués dans les performances, nous vous recommandons de toujours mesurer les résultats réels.However, because many factors are involved in performance, we recommend that you always measure actual results.

Éviter d’écrire à des emplacements de mémoire partagésAvoid Writing to Shared Memory Locations

Dans du code séquentiel, il n’est pas rare de lire des variables statiques ou d’écrire dans ces dernières ou dans des champs de classe.In sequential code, it is not uncommon to read from or write to static variables or class fields. Toutefois, l’accès simultané de plusieurs threads à de telles variables entraîne un fort risque d’engorgement.However, whenever multiple threads are accessing such variables concurrently, there is a big potential for race conditions. Bien que vous puissiez utiliser des verrous pour synchroniser l’accès à la variable, le coût de synchronisation peut nuire aux performances.Even though you can use locks to synchronize access to the variable, the cost of synchronization can hurt performance. Par conséquent, nous vous recommandons d’éviter, ou au moins de limiter autant que possible l’accès à un état partagé dans une boucle parallèle.Therefore, we recommend that you avoid, or at least limit, access to shared state in a parallel loop as much as possible. La meilleure façon de procéder consiste à utiliser les surcharges de Parallel.For et Parallel.ForEach qui utilisent une variable System.Threading.ThreadLocal<T> pour stocker l’état local du thread pendant l’exécution de la boucle.The best way to do this is to use the overloads of Parallel.For and Parallel.ForEach that use a System.Threading.ThreadLocal<T> variable to store thread-local state during loop execution. Pour plus d'informations, voir Procédure : écrire une boucle Parallel.For avec des variables locales de thread et Guide pratique pour écrire une boucle Parallel.ForEach avec des variables locales de partition.For more information, see How to: Write a Parallel.For Loop with Thread-Local Variables and How to: Write a Parallel.ForEach Loop with Partition-Local Variables.

Éviter la surparallélisationAvoid Over-Parallelization

L’utilisation de boucles parallèles entraîne des coûts de surcharge liés au partitionnement de la collection source et à la synchronisation des threads de travail.By using parallel loops, you incur the overhead costs of partitioning the source collection and synchronizing the worker threads. Les avantages de la parallélisation sont également limités par le nombre de processeurs de l’ordinateur.The benefits of parallelization are further limited by the number of processors on the computer. L’exécution de plusieurs threads liés au calcul sur un seul processeur ne permet aucune accélération.There is no speedup to be gained by running multiple compute-bound threads on just one processor. Par conséquent, vous devez veiller à ne pas surparalléliser une boucle.Therefore, you must be careful not to over-parallelize a loop.

Les boucles imbriquées sont le scénario le plus courant dans lequel une surparallélisation peut se produire.The most common scenario in which over-parallelization can occur is in nested loops. Dans la plupart des cas, il est préférable de paralléliser uniquement la boucle externe, sauf si une ou plusieurs conditions suivantes s’appliquent :In most cases, it is best to parallelize only the outer loop unless one or more of the following conditions apply:

  • La boucle interne est réputée être très longue.The inner loop is known to be very long.

  • Vous effectuez un calcul coûteux sur chaque commande.You are performing an expensive computation on each order. (l’opération montrée dans l’exemple n’est pas coûteuse)(The operation shown in the example is not expensive.)

  • Le système cible est connu pour avoir suffisamment de processeurs pour gérer le nombre de threads produits en parallélisant la requête sur cust.Orders.The target system is known to have enough processors to handle the number of threads that will be produced by parallelizing the query on cust.Orders.

Dans tous les cas, le test et la mesure sont la meilleure façon de déterminer la forme de requête optimale.In all cases, the best way to determine the optimum query shape is to test and measure.

Éviter de faire appel aux méthodes qui ne sont pas thread-safeAvoid Calls to Non-Thread-Safe Methods

L’écriture dans des méthodes d’instance qui ne sont pas thread-safe à partir d’une boucle parallèle peut entraîner une corruption des données qui peut être détectée ou non dans votre programme.Writing to non-thread-safe instance methods from a parallel loop can lead to data corruption which may or may not go undetected in your program. Cela peut également entraîner des exceptions.It can also lead to exceptions. Dans l’exemple suivant, plusieurs threads tenteraient d’appeler simultanément la méthode FileStream.WriteByte, qui n’est pas prise en charge par la classe.In the following example, multiple threads would be attempting to call the FileStream.WriteByte method simultaneously, which is not supported by the class.

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))

Limiter les appels aux méthodes qui ne sont pas thread-safeLimit Calls to Thread-Safe Methods

La plupart des méthodes statiques du .NET Framework sont thread-safe et peuvent être appelées à partir de plusieurs threads simultanément.Most static methods in the .NET Framework are thread-safe and can be called from multiple threads concurrently. Toutefois, même dans ces cas, la synchronisation impliquée peut entraîner un ralentissement significatif de la requête.However, even in these cases, the synchronization involved can lead to significant slowdown in the query.

Notes

Vous pouvez le tester vous-même en insérant des appels à WriteLine dans vos requêtes.You can test for this yourself by inserting some calls to WriteLine in your queries. Bien que cette méthode soit utilisée dans les exemples de documentation destinés à la démonstration, ne l’utilisez pas dans les boucles parallèles, sauf si nécessaire.Although this method is used in the documentation examples for demonstration purposes, do not use it in parallel loops unless necessary.

Tenir compte des problèmes d’affinité de threadBe Aware of Thread Affinity Issues

Certaines technologies, par exemple, les composants STA (Single-Threaded Apartment), Windows Forms et Windows Presentation Foundation (WPF) imposent des restrictions d’affinité de thread qui requièrent l’exécution de code sur un thread spécifique.Some technologies, for example, COM interoperability for Single-Threaded Apartment (STA) components, Windows Forms, and Windows Presentation Foundation (WPF), impose thread affinity restrictions that require code to run on a specific thread. Par exemple, dans Windows Forms et WPF, un contrôle est uniquement accessible sur le thread sur lequel il a été créé.For example, in both Windows Forms and WPF, a control can only be accessed on the thread on which it was created. Cela signifie, par exemple, que vous ne pouvez pas mettre à jour un contrôle de liste à partir d’une boucle parallèle, sauf si vous configurez le planificateur de threads de sorte qu’il planifie le travail uniquement sur le thread d’interface utilisateur.This means, for example, that you cannot update a list control from a parallel loop unless you configure the thread scheduler to schedule work only on the UI thread. Pour plus d’informations, consultez Spécification d’un contexte de synchronisation.For more information, see Specifying a synchronization context.

Être vigilant lors de l’attente dans des délégués appelés par Parallel.InvokeUse Caution When Waiting in Delegates That Are Called by Parallel.Invoke

Dans certaines circonstances, la bibliothèque parallèle de tâches intègre une tâche, ce qui signifie qu’elle s’exécute sur la tâche du thread en cours d’exécution.In certain circumstances, the Task Parallel Library will inline a task, which means it runs on the task on the currently executing thread. Pour plus d’informations, consultez l’article Planificateurs de tâches. Cette optimisation des performances peut provoquer un interblocage dans certains cas.(For more information, see Task Schedulers.) This performance optimization can lead to deadlock in certain cases. Par exemple, deux tâches peuvent exécuter le même code de délégué, qui signale la survenue d’un événement et attend l’autre tâche à signaler.For example, two tasks might run the same delegate code, which signals when an event occurs, and then waits for the other task to signal. Si la seconde tâche est incluse sur le même thread que la première et que cette dernière passe à l’état En attente, la seconde tâche ne pourra jamais signaler son événement.If the second task is inlined on the same thread as the first, and the first goes into a Wait state, the second task will never be able to signal its event. Pour éviter une telle situation, vous pouvez spécifier un délai d’expiration sur l’opération d’attente, ou utiliser les constructeurs de thread explicites pour s’assurer qu’une tâche ne peut pas bloquer l’autre.To avoid such an occurrence, you can specify a timeout on the Wait operation, or use explicit thread constructors to help ensure that one task cannot block the other.

Ne pas supposer que les itérations de ForEach, For et ForAll s’exécutent toujours en parallèleDo Not Assume that Iterations of ForEach, For and ForAll Always Execute in Parallel

Il est important de garder à l’esprit que les itérations individuelles dans une boucle For, ForEach ou ForAll peuvent, mais ne doivent pas forcément, s’exécuter en parallèle.It is important to keep in mind that individual iterations in a For, ForEach or ForAll loop may but do not have to execute in parallel. Par conséquent, vous devez éviter d’écrire du code dont l’exactitude dépend de l’exécution parallèle d’itérations ou de l’exécution d’itérations dans un ordre particulier.Therefore, you should avoid writing any code that depends for correctness on parallel execution of iterations or on the execution of iterations in any particular order. Par exemple, ce code est susceptible d’interbloquer :For example, this code is likely to deadlock:

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
Dim mres = 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)
                mres.Set()
            Else
                Console.WriteLine("Waiting on {0} with value of {1}",
                                  Thread.CurrentThread.ManagedThreadId, j)
                mres.Wait()
            End If
        End Sub) ' deadlocks

Dans cet exemple, une itération définit un événement que toutes les autres itérations attendent.In this example, one iteration sets an event, and all other iterations wait on the event. 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.None of the waiting iterations can complete until the event-setting iteration has completed. Toutefois, il est possible que les itérations en attente bloquent tous les threads utilisés pour exécuter la boucle parallèle, avant que l’itération de définition d’événement ait eu une chance de s’exécuter.However, it is possible that the waiting iterations block all threads that are used to execute the parallel loop, before the event-setting iteration has had a chance to execute. Cela provoque un interblocage : l’itération de définition d’événement ne s’exécute jamais et les itérations en attente ne s’activent pas non plus.This results in a deadlock – the event-setting iteration will never execute, and the waiting iterations will never wake up.

En particulier, une itération de boucle parallèle ne doit jamais attendre une autre itération de la boucle pour progresser.In particular, one iteration of a parallel loop should never wait on another iteration of the loop to make progress. Si la boucle parallèle décide de planifier les itérations de manière séquentielle, mais dans l’ordre inverse, un interblocage se produit.If the parallel loop decides to schedule the iterations sequentially but in the opposite order, a deadlock will occur.

Éviter d’exécuter des boucles parallèles sur le thread d’interface utilisateurAvoid Executing Parallel Loops on the UI Thread

Il est important de maintenir la réactivité de l’interface utilisateur de l’application (IU).It is important to keep your application's user interface (UI) responsive. Si une opération contient suffisamment de travail pour assurer la parallélisation, cette opération ne doit pas être exécutée sur le thread d’interface utilisateur.If an operation contains enough work to warrant parallelization, then it likely should not be run that operation on the UI thread. Au lieu de cela, elle doit décharger cette opération de sorte qu’elle s’exécute sur un thread d’arrière-plan.Instead, it should offload that operation to be run on a background thread. Par exemple, si vous souhaitez utiliser une boucle parallèle pour calculer des données qui doivent ensuite être restituées dans un contrôle d’interface utilisateur, vous devez envisager l’exécution de la boucle dans une instance de tâche plutôt que directement dans un gestionnaire d’événements de l’interface utilisateur.For example, if you want to use a parallel loop to compute some data that should then be rendered into a UI control, you should consider executing the loop within a task instance rather than directly in a UI event handler. Vous ne devez marshaler la mise à jour de l’interface utilisateur vers le thread de l’interface utilisateur qu’une fois le calcul principal terminé.Only when the core computation has completed should you then marshal the UI update back to the UI thread.

Si vous exécutez des boucles parallèles sur le thread d’interface utilisateur, veillez à éviter la mise à jour des contrôles d’interface utilisateur à partir de la boucle.If you do run parallel loops on the UI thread, be careful to avoid updating UI controls from within the loop. Toute tentative de mise à jour des contrôles d’interface utilisateur à partir d’une boucle parallèle qui s’exécute sur le thread d’interface utilisateur peut entraîner une altération de l’état, des exceptions, des reports de mise à jour et même des interblocages, selon la manière dont la mise à jour de l’interface utilisateur est appelée.Attempting to update UI controls from within a parallel loop that is executing on the UI thread can lead to state corruption, exceptions, delayed updates, and even deadlocks, depending on how the UI update is invoked. Dans l’exemple suivant, la boucle parallèle bloque le thread d’interface utilisateur sur lequel elle s’exécute jusqu’à ce que toutes les itérations soient terminées.In the following example, the parallel loop blocks the UI thread on which it’s executing until all iterations are complete. Toutefois, si une itération de la boucle s’exécute sur un thread d’arrière-plan (comme For peut le faire), l’appel à Invoke entraîne l’envoi d’un message au thread d’interface utilisateur et bloque en attendant que ce message soit traité.However, if an iteration of the loop is running on a background thread (as For may do), the call to Invoke causes a message to be submitted to the UI thread and blocks waiting for that message to be processed. Étant donné que le thread d’interface utilisateur est bloqué lors de l’exécution de For, le message ne peut jamais être traité et le thread d’interface utilisateur est soumis à des interblocages.Since the UI thread is blocked running the For, the message can never be processed, and the UI thread deadlocks.

private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Parallel.For(0, iterations, Sub(x)
                                    Button1.Invoke(Sub()
                                                       DisplayProgress(x)
                                                   End Sub)
                                End Sub)
End Sub

L’exemple suivant montre comment éviter l’interblocage, en exécutant la boucle à l’intérieur d’une instance de tâche.The following example shows how to avoid the deadlock, by running the loop inside a task instance. Le thread d’interface utilisateur n’est pas bloqué par la boucle, et le message peut être traité.The UI thread is not blocked by the loop, and the message can be processed.

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
        Parallel.For(0, N, i =>
        {
            // do work for i
            button1.Invoke((Action)delegate { DisplayProgress(i); });
        })
         );
}
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
                                                                Button1.Invoke(Sub()
                                                                                   DisplayProgress(x)
                                                                               End Sub)
                                                            End Sub))
End Sub

Voir aussiSee also