Programmation parallèle et multi-coeur avec .NET

Optimisation du code géré pour les machines multi-coeur, `Parallel FX'.

Préambule

C'est juste une petite présentation de la TPL (Task Parallel Library).
Note : j'ai mis très peu d'exemples puisque les classes de la TPL me semble suffisamment parlantes... Si vous avez toutefois des questions n'hésitez pas sans toutefois oublier que la TPL n'est pas figée.

La TPL

La TPL (Task Parallel Library) est conçue pour faciliter l'écriture de code géré capable d'utiliser plusieurs processeurs automatiquement. À l'aide de cette bibliothèque, vous pouvez facilement exprimer le parallélisme potentiel d'un code séquentiel existant, où les tâches parallèles exposées seront exécutées simultanément sur tous les processeurs disponibles.

Information MSDN : http://msdn.microsoft.com/en-us/library/dd460693(VS.100).aspx

Espace de nom : System.Threading.

Il faut installer 'Microsoft Parallel Extensions to .NET Framework 3.5 CTP'. Puis faire référence dans son projet à la dll du dossier d'installation : 'C:\Program Files\Microsoft Parallel Extensions ... CTP\System.Threading.dll '.

Les méthodes proposées

Itérations indépendantes les unes des autres

Parallel.For(<début>, <fin>, delegate(<type> i) {<..actions..>});

Itérations parallèles en concurrence pour le même verrou (pour C#4)

Ex:

  • une somme conditionnelle sur une variable : var i = Parallel.Aggregate(<début>, <fin>, <valeur de départ>, <..action sur chacun des éléments..>, <..action au final..>);
  • pour '<..sur chacun des éléments..>' : delegate(int x) { return x*10; }
  • '<..action au final..>' : delegate(int x, int y) { return x+y; }

Lancer des tâches en parallèle

Parallel.Invoke(<..action1..>, <..action2..>, ...); (anciennement Parallel.Do(<..action1..>, <..action2..>, ...);)
Ou sous forme d'un tableau de tâches : Task[] tasks = { Task.Factory.StartNew(<..action1..>), .. }; qui pourrait être suivi de Task.WaitAll(tasks); pour une attente de fin de traitement.

Tâches parallèles générales

Elles s'appuient sur la class Task. On peut la comparer à Thread mais en fait elle permet d'avantage.

class Task
{
    Task( Action action );

    void Wait();
    void Cancel();
    bool IsCompleted { get; }
    ...
}

Note : toutes les méthodes statiques de la classe Parallel utilisent l'objet Task.

Tâches qui calculent et retournent un résultat

Un `Future' est une tâche qui calcule un résultat. Ce résultat est un délégué avec le type Func<T>T est le type de valeur du futur.

Future<type> action = Future.Create(<..action..>); puis action.Value;

Le résultat du futur est récupéré par la propriété Value. La propriété Value appelle Wait en interne pour s'assurer que la tâche est achevée et que la valeur de résultat a été calculée.

De même : var action = Task<type>.Factory.StartNew(<..action..>); puis pour avoir le résultat : action.Result;

Les exceptions

Toute exception levée dans l'action associée est stockée dans une tâche et levée à nouveau lorsque Wait est appelé. De même, les fonctions Parallel.For et Parallel.Do accumulent toutes les exceptions déclenchées et les lèvent à nouveau lorsque toutes les tâches sont terminées. Ceci garantit que les exceptions ne sont jamais perdues et sont correctement propagées aux dépendants.

Gestionnaire des tâches

Le Pool de threads est géré automatiquement par la TPL : Toutes les tâches appartiennent à un gestionnaire des tâches, qui, comme son nom l'indique, gère les tâches et veille à ce que les threads de travail exécutent ces tâches. Si un gestionnaire des tâches est toujours disponible par défaut, une application peut également en créer un de manière explicite.

Traitement des données

Données, de LINQ à PLINQ

Outre l'écriture des requêtes LINQ de la même façon que vous le feriez LINQ, il y a deux étapes supplémentaires nécessaires à l'utilisation de PLINQ :

1. Référencer l'assembly System.Concurrency.dll pendant la compilation.

2. Encapsuler votre source de données dans un élément IParallelEnumerable<T> avec un appel de la méthode d'extension System.Linq.ParallelEnumerable.AsParallel.

L'appel de la méthode d'extension AsParallel à l'étape 2 s'assure que le compilateur C# ou Visual Basic est relié à la version System.Linq.ParallelEnumerable des opérateurs de requête standard au lieu de System.Linq.Enumerable. Cela permet à PLINQ de prendre le contrôle et d'exécuter la requête en parallèle. AsParallel est défini comme utilisant n'importe quel IEnumerable<T>.

Ex :

var a = from élmt in list // avec list un List<int>
where élmt < 8
select élmt;

devient :

 var a = from élmt in list.AsParallel()
where élmt < 8
select élmt;

Attention, après une opération (ou sélection élaborée) sur les éléments la conservation de l'ordre n'est pas garantie.

Collections

Dans System.Collections.Concurrent

  • BlockingCollection<T> (IProducerConsumerCollection<T>)
  • ConcurrentBag<T>
  • ConcurrentDictionary<TKey, TValue>
  • ConcurrentQueue<T> (anciennement ConcurrentLinkedList<T>)
  • ConcurrentStack<T>

Synchronisation

Dans System.Threading

  • Barrier qui permet à plusieurs threads de travailler sur un algorithme en parallèle, puis pour chacun, de signaler son arrivée et ensuite de bloquer jusqu'à ce que certaines ou toutes les tâches soient arrivées.
  • CountdownEvent
  • ManualResetEventSlim comme ManualResetEvent
  • SemaphoreSlim, limite le nombre de thread pour avoir un acces concurrent à la ressource
  • SpinLock comme lock
  • SpinWait
  • WriteOnce<T>
  • LazyInit<T>

Permettre que la mémoire d'un objet ne soit pas attribuée tant que ce n'est pas nécessaire : c'est le Lazy

  • System.Lazy<T> (anciennement System.LazyVariable<T>)
  • System.Threading.ThreadLocal<T>
  • System.Threading.LazyInitializer

Conclusion

Les réticences envers la programmation asynchrone (et à plus forte raison avec le multi coeur) sont nombreuses pour beaucoup de programmeurs : difficulté de conception, de maintenance, de maitrise sur le débogage ou l'exécution... Ainsi qui n'a jamais oublié de protéger les accès à un objet `perdu' dans une méthode ?

Pourtant nous connaissons aussi les bienfaits qui en découlent : gains potentiels de performance, utilisation d'un système graphique, etc.

La TPL est appelé à évoluer et les classes -leur nom, leur nombre, leurs méthodes, ...- ne sont pas figées. C'est pourquoi elle est marqué CTP (Community Technology Preview, une version "beta" en quelque sorte). Malgré ce caractère non finalisé -assumé par Microsoft- elle promet une simplification de la programmation (multithread et) multi coeur.

Les gains de performance annoncés avec la multiplication des machines équipées des plusieurs coeurs est sans équivoque. Cependant doit on basculer dans le tout parallèle au niveau du code ? Non et ce pour plusieurs raisons :

Paralléliser son code reste très difficile même si la syntaxe de la TPL simplifie les choses. Or ce temps d'adaptation peut devenir inacceptable.
Généralement, seule une petite partie critique du programme réclame une optimisation en terme de vitesse.
L'initialisation de la TPL prend un certain temps et rend son utilisation inappropriée pour les touts petits traitements.

Il faut donc raison garder mais on peut saluer cette évolution (future) de .Net... Vivement C#4 !!

Ce document intitulé « Programmation parallèle et multi-coeur avec .NET » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous