Patrice Lamarche
Découverte de ParallelFX
Découverte du framework ParallelFX, de la Task Parallel Library et de PLINQ
Par Patrice Lamarche publié le 09/12/2007 à 22:39, lu 4640 fois, 4 pages
 2 | System.Threading
ParallelFX est composé de plusieurs modules dont le principal est la Task Parallel Library (TPL). Cette couche gère toute la logique de création de tâches, d'exécution en parallèle, la logique d'annulation, une gestion d'exception particulière, etc. Afin d'accéder à cette TPL et de fournir un accès de plus haut niveau à celle-ci, le namespace System.Threading a été étendu grâce à l'assembly System.Threading.dll ajouté dans le Global Assembly Cache lors de l'installation de ParallelFX. Ce namespace se voit ainsi ajouté de nouveaux objets, ainsi qu'un nouveau namespace System.Threading.Tasks qui contient précisément cette TPL.
Ce namespace contient donc la cuisine interne de gestion d'exécution parallèle et non séquentielle. L'élément le plus important est le TaskManager. Le comportement par défaut du TaskManager est de créer un thread par coeur, chaque thread ayant une queue d'éléments à exécuter (des délégués). De plus ce TaskManager utilise des algorithmes d'optimisation afin de répartir le travail équitablement entre chaque thread, en permettant par exemple à un thread ayant sa file d'attente de délégués vide, de récupérer une partie de ceux des autres threads gérés par le TaskManager.
Afin d'utiliser de manière simple la TPL, la classe Parallel vous fournit 3 méthodes permettant d'effectuer des boucles dont les itérations ne seront exécutées séquentiellement mais de manière parrallèle. La classe Parallel vous permet de créer 3 types de boucles : do, for et foreach, ces deux derniers ayant également une version générique.
Afin de découvrir la simplicité de cette API, prenons de suite un exemple de calcul arithmétique que nous allons ralentir à l'aide d'un Thread.Sleep afin que l'on puisse apprécier le gain apporté par ParallelFx.

static int Calcul(int i)

{

    System.Threading.Thread.Sleep(100);

    return i * i;

}

Cette fonction permet de calculer le carré d'un entier et nous allons l'appeler de deux manières différentes. Pour le premier exemple, nous allons appeler cette fonction dans une boucle de 0 à 100 de manière séquentielle grâce à une traditionnelle boucle for.

static void Main(string[] args)

{

 

    Stopwatch w = new Stopwatch();

    w.Start();

 

    for (int i = 0; i < 100; i++)

    {

        Console.WriteLine(string.Format("i:{0} ²:{1} ThreadId:{2}", i.ToString(), Calcul(i).ToString(),

            Thread.CurrentThread.ManagedThreadId.ToString()));

    }

 

    Console.WriteLine("Ticks :" + w.ElapsedTicks.ToString());

    Console.ReadLine();

}

En plus d'effectuer le calcul, nous affichons la valeur de i à chaque itération ainsi que l'ID du thread qui exécute le code. Nous récupérons également le temps d'exécution de ce traitement grâce un objet Stopwatch présent dans le namespace System.Diagnostics.
Le résultat est le suivant :
 
/content/c21100b0-d02a-45f9-bb19-5c0fce18f0b4/1.jpg
 
Sans surprise, un seul thread est utilisé pour exécuter le code, le thread principal de l'application, et le traitement mets plus de 144 millions de ticks pour être exécuter.
Chaque itération de notre boucle for étant indépendante des unes des autres, nous pouvons utiliser ParallelFX afin de paralléliser leur traitement. Effectuons donc à présent le même traitement mais en utilisant ParallelFx et plus précisément la TPL. Pour cela nous allons légèrement modifier notre boucle for afin de l'implémenter via la méthode for proposée par la classe Parallel :

Parallel.For(0, 100, delegate(int i)

      {

          Console.WriteLine(string.Format("i:{0} ²:{1} ThreadId:{2}", i.ToString(), Calcul(i).ToString(),

              Thread.CurrentThread.ManagedThreadId.ToString()));

      }

      );

La signature la plus simple de cette méthode For prend en paramètre la borne de départ et de fin de la boucle, ainsi qu'un délégué contenant le code à exécuter.
Ce code peut être écrit pour n'importe quelle architecture, même les monoprocesseurs, si vous vous trouvez dans ce cas là, vous devriez avoir les mêmes résultats qu'une boucle for classique. Dans mon cas, ma machine est à un Core 2 Duo, les performances devraient donc être meilleures qu'une boucle for monothreadée :
 
/content/c21100b0-d02a-45f9-bb19-5c0fce18f0b4/2.jpg
 
Premier constat le traitement a été bien plus rapidement exécuté que la première version écrite avec une boucle for classique puisque le temps d'exécution est de 33 millions de ticks contre 144 précédemment. On peut voir que ce temps d'exécution a été amélioré grâce à la création de différents threads que l'on peut dissocier grâce à l'id affichée à la fin de chaque ligne. Ainsi les itérations de notre boucle ont été découpées en différentes tâches exécutées par différents threads exécutés en parallèle grâce aux différents coeurs du micro processeur. Tout cela a de plus été réalisé sans jamais avoir à explicitement créér la logique de création de thread, de découpage des tâches, tout ceci étant automatiquement géré par ParallelFX.
La méthode For expose 9 surcharges différentes dont certaines permettent notamment de partager un état entre les différentes itérations, ces différentes surcharges seront traitées dans un prochain article plus avancé qui sera rédigé lorsque ParallelFX sera proposé en version complète probablement via une prochaine CTP. Attardons nous maintenant à la seconde API de haut niveau offerte avec la CTP de décembre pour écrire des traitements parallèles : PLinq.
 
» Démarrer une discussion