AllInfo

Comment multithread en toute sécurité et efficacité dans .NET

Le multithreading peut être utilisé pour accélérer considérablement les performances de votre application, mais aucune accélération n'est gratuite— la gestion des threads parallèles nécessite une programmation minutieuse, et sans les précautions appropriées, vous pouvez rencontrer des conditions de concurrence, des blocages et même des plantages.

Qu'est-ce qui rend le multithreading difficile ?

Sauf indication contraire de votre part, tout votre code s'exécute sur le “thread principal.” Depuis le point d'entrée de votre application, il parcourt et exécute toutes vos fonctions les unes après les autres. Cela a une limite aux performances, car vous ne pouvez évidemment pas faire grand-chose si vous devez tout traiter un à la fois. La plupart des processeurs modernes ont six cœurs ou plus avec 12 threads ou plus, il reste donc des performances sur la table si vous ne les utilisez pas.

Cependant, ce n'est pas aussi simple que d'activer le multithreading. Seules des choses spécifiques (telles que des boucles) peuvent être correctement multithreadées, et il y a beaucoup de considérations à prendre en compte lors de cette opération.

Le premier et le plus important est les conditions de course< /fort>. Ceux-ci se produisent souvent pendant les opérations d'écriture, lorsqu'un thread modifie une ressource partagée par plusieurs threads. Cela conduit à un comportement où la sortie du programme dépend du thread qui termine ou modifie quelque chose en premier, ce qui peut conduire à un comportement aléatoire et inattendu.

Celles-ci peuvent être très, très simples, par exemple, vous devez peut-être garder un compte courant de quelque chose entre les boucles. Le moyen le plus évident de le faire est de créer une variable et de l'incrémenter, mais ce n'est pas thread-safe.

Publicité

Cette condition de concurrence critique se produit parce qu'il ne s'agit pas simplement d'en ajouter un à la variable. dans un sens abstrait ; le processeur charge la valeur du nombre dans le registre, en ajoute un à cette valeur, puis stocke le résultat en tant que nouvelle valeur de la variable. Il ne sait pas qu'entre-temps, un autre thread essayait également de faire exactement la même chose et chargeait une valeur de nombre bientôt incorrecte. Les deux threads sont en conflit, et à la fin de la boucle, le nombre peut ne pas être égal à 100.

.NET fournit une fonctionnalité pour aider à gérer cela : le mot-clé de verrouillage. Cela n'empêche pas d'apporter des modifications, mais cela aide à gérer la concurrence en n'autorisant qu'un seul thread à la fois à obtenir le verrou. Si un autre thread essaie d'entrer une instruction de verrouillage pendant qu'un autre thread est en cours de traitement, il attendra jusqu'à 300 ms avant de continuer.

Vous ne pouvez verrouiller que les types de référence, un modèle commun est donc créé. un objet de verrouillage au préalable, et en l'utilisant comme substitut au verrouillage du type de valeur.

Cependant , vous remarquerez peut-être qu'il y a maintenant un autre problème : les blocages. Ce code est un exemple du pire des cas, mais ici, c'est presque exactement la même chose que de faire une boucle for normale (en fait un peu plus lent, car les threads et les verrous supplémentaires sont une surcharge supplémentaire). Chaque thread essaie d'obtenir le verrou, mais un seul à la fois peut avoir le verrou, donc un seul thread à la fois peut réellement exécuter le code à l'intérieur du verrou. Dans ce cas, c'est tout le code de la boucle, donc l'instruction de verrouillage supprime tous les avantages du threading et ralentit tout simplement.

En règle générale, vous souhaitez verrouiller au besoin chaque fois que vous devez effectuer des écritures. Cependant, vous voudrez garder à l'esprit la simultanéité lors du choix de ce qu'il faut verrouiller, car les lectures ne sont pas toujours thread-safe non plus. Si un autre thread écrit dans l'objet, le lire à partir d'un autre thread peut donner une valeur incorrecte ou provoquer le renvoi d'une condition particulière à un résultat incorrect.

Heureusement, il existe quelques astuces pour le faire correctement où vous pouvez équilibrer la vitesse du multithreading tout en utilisant des verrous pour éviter les conditions de concurrence.

Utiliser le verrouillage pour les opérations atomiques

Pour les opérations de base, l'utilisation de l'instruction de verrouillage peut être excessive. Bien qu'il soit très utile pour verrouiller avant des modifications complexes, il est trop lourd pour quelque chose d'aussi simple que d'ajouter ou de remplacer une valeur.

Publicité

Interlocked est une classe qui encapsule certaines opérations de mémoire telles que l'ajout, le remplacement et la comparaison. Les méthodes sous-jacentes sont implémentées au niveau du processeur et garanties d'être atomiques, et beaucoup plus rapides que l'instruction de verrouillage standard. Vous voudrez les utiliser autant que possible, même s'ils ne remplaceront pas entièrement le verrouillage.

Dans l'exemple ci-dessus, le remplacement du verrou par un appel à Interlocked.Add() accélérera beaucoup l'opération. Bien que cet exemple simple ne soit pas plus rapide que de ne pas utiliser Interlocked, il est utile dans le cadre d'une opération plus vaste et constitue toujours une accélération.

Il existe également des incréments et des décrémentations pour les opérations ++ et –, ce qui vous fera économiser deux solides frappes. Ils enveloppent littéralement Add(ref count, 1) sous le capot, donc il n'y a pas d'accélération spécifique pour les utiliser.

Vous pouvez également utiliser Exchange, une méthode générique qui définira une variable égale à la valeur qui lui est transmise. Cependant, vous devez être prudent avec celui-ci, si vous le définissez sur une valeur que vous avez calculée à l'aide de la valeur d'origine, ce n'est pas thread-safe, car l'ancienne valeur aurait pu être modifiée avant d'exécuter Interlocked. .Exchange.

CompareExchange vérifiera l'égalité de deux valeurs et remplacera la valeur si elles&# 8217 ; sont égaux.

Utiliser les collections Thread Safe

Les collections par défaut dans System.Collections.Generic peuvent être utilisées avec le multithreading, mais elles ne sont pas entièrement thread-safe. Microsoft fournit des implémentations thread-safe de certaines collections dans System.Collections.Concurrent.

Publicité

Parmi celles-ci, citons ConcurrentBag, une collection générique non ordonnée, et ConcurrentDictionary, un dictionnaire thread-safe. Il existe également des files d'attente et des piles simultanées, et OrderablePartitioner, qui peut diviser les sources de données commandables telles que les listes en partitions distinctes pour chaque thread.

Regardez pour paralléliser les boucles< /h2>

Souvent, l'endroit le plus simple pour le multithread est dans les grandes boucles coûteuses. Si vous pouvez exécuter plusieurs options en parallèle, vous pouvez obtenir une accélération considérable du temps d'exécution global.

La meilleure façon de gérer cela est avec System.Threading.Tasks.Parallel. Cette classe fournit des remplacements pour les boucles for et foreach qui exécutent les corps de boucle sur des threads distincts. Il est simple à utiliser, mais nécessite une syntaxe légèrement différente :

De toute évidence, le problème ici est que vous devez vous assurer que DoSomething() est thread-safe et n'interfère avec aucune variable partagée. Cependant, ce n'est pas toujours aussi simple que de simplement remplacer la boucle par une boucle parallèle, et dans de nombreux cas, vous devez verrouiller les objets partagés pour apporter des modifications.

Pour atténuer certains des problèmes liés aux blocages, Parallel.For et Parallel.ForEach fournissent des fonctionnalités supplémentaires pour gérer l'état. Fondamentalement, toutes les itérations ne s'exécuteront pas sur un thread séparé & #8212;si vous avez 1000 éléments, cela ne créera pas 1 000 threads ; il va créer autant de threads que votre CPU peut en gérer et exécuter plusieurs itérations par thread. Cela signifie que si vous calculez un total, vous n'avez pas besoin de verrouiller pour chaque itération. Vous pouvez simplement passer une variable de sous-total et, à la toute fin, verrouiller l'objet et apporter des modifications une fois. Cela réduit considérablement les frais généraux sur les très grandes listes.

Publicité

Jetons un coup d'œil à un exemple. Le code suivant prend une grande liste d'objets et doit sérialiser chacun séparément en JSON, se terminant par une liste<string> de tous les objets. La sérialisation JSON est un processus très lent, donc diviser chaque élément sur plusieurs threads est une grande accélération.

< p>Il y a un tas d'arguments, et beaucoup à déballer ici :

Unity Multithreading

One note finale—si vous utilisez le moteur de jeu Unity, vous devrez faire attention au multithreading. Vous ne pouvez pas appeler d'API Unity, sinon le jeu plantera. Il est possible de l'utiliser avec parcimonie en effectuant des opérations d'API sur le thread principal et en alternant chaque fois que vous avez besoin de paralléliser quelque chose.

Cela s'applique principalement aux opérations qui interagissent avec la scène ou la physique. moteur. Les mathématiques Vector3 ne sont pas affectées et vous êtes libre de l'utiliser à partir d'un thread séparé sans problème. Vous êtes également libre de modifier les champs et les propriétés de vos propres objets, à condition qu'ils n'appellent aucune opération Unity sous le capot.

Exit mobile version