Pièges potentiels dans le parallélisme des données et des tâches

Dans de nombreux cas, Parallel.For et Parallel.ForEach peuvent améliorer les performances de manière significative sur des boucles séquentielles ordinaires. Toutefois, la mise en parallèle de la boucle présente une certaine complexité pouvant entraîner des problèmes qui, dans du code séquentiel, ne sont que peu voire jamais rencontrés. Cette rubrique répertorie les pratiques à éviter lorsque vous écrivez des boucles parallèles.

Parallèle n'est pas forcément synonyme de rapidité

Dans certains cas, une boucle parallèle peut s'exécuter plus lentement que son équivalent séquentiel. La règle empirique de base est que les boucles parallèles qui ont peu d'itérations et des délégués utilisateurs rapides sont nettement moins susceptibles de s'accélérer. Toutefois, étant donné que de nombreux facteurs sont impliqués dans la performance, nous vous recommandons de mesurer toujours des résultats réels.

Éviter d'écrire dans les zones de mémoire partagées

Dans du code séquentiel, il n'est pas rare de lire ou d'écrire dans les variables statiques ou les champs de classe. Toutefois, lorsque plusieurs threads accèdent simultanément à de telles variables, les conditions de concurrence sont favorisées. Bien qu'il soit possible d'utiliser des verrous pour synchroniser l'accès à la variable, le coût de synchronisation peut réduire les performances. Par conséquent, il est recommandé d'éviter, ou du moins de limiter autant que possible, l'accès à l'état partagé dans une boucle parallèle. Pour ce faire, la meilleure méthode consiste à utiliser les surcharges de Parallel.For et Parallel.ForEach qui utilisent une variable System.Threading.ThreadLocal<T> pour stocker l'état thread local pendant l'exécution de la boucle. Pour plus d'informations, consultez Comment : écrire une boucle Parallel.For comprenant des variables locales de thread et Comment : écrire une boucle Parallel.ForEach comprenant des variables de thread local.

Éviter la surparallélisation

En utilisant les boucles parallèles, vous entraînez des coûts de partitionnement de la collection source et de synchronisation des threads de travail. Les avantages de la parallélisation sont également limités par le nombre de processeurs de l'ordinateur. L'exécution de plusieurs threads orientés ordinateur sur un seul processeur n'accélère pas l'exécution. Par conséquent, veillez à éviter la surparallélisation d'une boucle.

La surparallélisation se produit le plus souvent dans des boucles imbriquées. Dans la plupart des cas, il vaut mieux paralléliser uniquement la boucle externe à moins qu'une ou plusieurs des conditions suivantes ne s'appliquent :

  • La boucle interne est réputée pour être très longue.

  • Vous exécutez un calcul coûteux sur chaque ordre (l'opération affichée dans cet 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 produits en parallélisant la requête sur cust.Orders.

Dans tous les cas, la meilleure méthode pour déterminer la forme de requête optimale est de la tester et la mesurer.

Éviter les appels à des méthodes non thread-safe

L'écriture dans des méthodes d'instance non thread-safe depuis une boucle parallèle peut mener à des données endommagées, susceptibles de ne pas être détectées dans votre programme. Elle peut également provoquer des exceptions. Dans l'exemple suivant, plusieurs threads essaient d'appeler simultanément la méthode FileStream.WriteByte, qui n'est pas prise en charge par la classe.

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)))
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));

Limiter les appels à des méthodes thread-safe

La plupart des méthodes statiques du .NET Framework sont thread-safe et peuvent être appelées simultanément par plusieurs threads. Toutefois, même dans ce cas-là, la synchronisation impliquée peut mener à un ralentissement significatif de la requête.

RemarqueRemarque

Vous pouvez tester ceci vous-même en insérant des appels à WriteLine dans vos requêtes.Bien que cette méthode soit utilisée à des fins de démonstration dans les exemples de documentation, ne l'utilisez pas dans les boucles parallèles sauf en cas de nécessité.

Connaître les problèmes d'affinité de thread

Certaines technologies, telles que l'interopérabilité COM pour les composants STA, Windows Forms et Windows Presentation Foundation (WPF), imposent des restrictions d'affinité de thread qui nécessitent que le code soit exécuté sur un thread spécifique. Par exemple, un contrôle est accessible uniquement sur le thread sur lequel il a été créé dans Windows Forms et WPF. Par exemple, cela signifie que vous ne pouvez pas mettre à jour un contrôle de liste à partir d'une boucle parallèle à moins de configurer le planificateur de thread pour qu'il planifie uniquement le travail sur le thread d'interface utilisateur. Pour plus d'informations, consultez Comment : planifier le travail sur un contexte de synchronisation spécifié.

Utiliser l'avertissement lors de l'attente dans les délégués appelés par Parallel.Invoke

Dans certaines circonstances, la bibliothèque parallèle de tâches veut inclure une tâche, ce qui signifie qu'elle s'exécute sur la tâche sur le thread en cours d'exécution. (Pour plus d'informations, consultez Planificateurs de tâches). Cette optimisation des performances peut mener à l'interblocage dans certains cas. Par exemple, deux tâches peuvent exécuter le même code de délégué, qui signale lorsqu'un événement se produit, puis attend que l'autre tâche envoie un signal. Si la seconde tâche est incluse sur le même thread que la première, et que la première passe à un état d'attente, la seconde tâche ne sera jamais en mesure de signaler son événement. Pour éviter que cela ne se produise, vous pouvez spécifier un délai d'attente sur l'opération d'attente, ou utilisez des constructeurs de thread explicites pour aider à garantir qu'une tâche ne peut pas bloquer l'autre.

Ne supposez pas que les itérations de ForEach, For et ForAll s'exécutent toujours en parallèle

Il est important de ne pas oublier que les itérations individuelles dans une boucle For, ForEach ou ForAll<TSource> peuvent s'exécuter en parallèle, mais n'ont aucune obligation de le faire. 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. Par exemple, ce code risque l'interblocage :

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
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 cours d'attente ne peut se terminer tant que l'itération définissant l'événement ne l'est pas elle-même. Toutefois, il est possible que les itérations en cours d'attente bloquent tous les threads utilisés pour exécuter la boucle parallèle, avant la fin de l'exécution de l'itération définissant l'événement. Cela provoque un interblocage, autrement dit, l'itération définissant l'é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 poursuivre sa progression. 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 produira.

Comment éviter l'exécution de boucles parallèles sur le thread d'interface utilisateur

Il est important que l'interface utilisateur de votre application reste réactive. Si une opération contient suffisamment de travail pour assurer la parallélisation, elle ne devrait probablement pas être exécutée sur le thread d'interface utilisateur. À la place, cette opération devrait être déchargée afin d'être exécutée sur un thread d'arrière-plan. Par exemple, si vous souhaitez utiliser une boucle parallèle pour calculer certaines données qui doivent être restituées ensuite dans un contrôle d'interface utilisateur, exécutez la boucle dans une instance de tâche plutôt que directement dans un gestionnaire d'événements d'interface utilisateur. Ce n'est qu'à la fin du calcul principal que vous devez remarshaler la mise à jour de l'interface utilisateur au thread d'interface utilisateur.

Si vous exécutez des boucles parallèles sur le thread d'interface utilisateur, essayez d'éviter de mettre à jour des contrôles d'interface utilisateur depuis l'intérieur de la boucle. La tentative de mise à jour des contrôles d'interface utilisateur depuis l'intérieur d'une boucle parallèle qui exécute le thread d'interface utilisateur peut entraîner l'altération de l'état, des exceptions, des mises à jour retardées y compris des interblocages, selon la façon dont la mise à jour de l'interface utilisateur est appelée. 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. 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 l'attente pour que ce message soit traité. Puisque 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 donne lieu à un interblocage.

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
private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}

L'exemple suivant indique comment éviter l'interblocage, en exécutant la boucle à l'intérieur d'une instance de tâche. Le thread d'interface utilisateur n'est pas bloqué par la boucle et le message peut être traité.

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

Voir aussi

Concepts

Programmation parallèle dans le .NET Framework

Pièges potentiels avec PLINQ

Autres ressources

Modèles de programmation parallèle : présentation et application des modèles parallèles avec le .NET Framework 4 (page éventuellement en anglais)