TPL in a real project

31. janvier 2011

Pour mettre un peu en pratique la TPL (Task Parallel Library), j’ai décidé de développer un petit moteur de raytracing.

image

En effet, la technique de raytracing est une technique ultra parallélisable puisque chaque pixel lance son propre rayon et de ce fait est entièrement autonome.

Ainsi si j’ai 800x600 processeurs, je peux lancer en parallèle le shoot d’une image de 800x600.

Bon évidemment c’est de la théorie mais déjà en pratique avec mon core Intel I7 je dispose de 8 cœurs et donc potentiellement de 8 threads totalement indépendants.

L’algorithme général du raytracing consiste donc à faire une double boucle parcourant les x et les y de l’image et lançant un rayon pour chaque pixel.

Cela donne donc à peu prés ceci:

for (int y = 0; y < ScreenHeight; y++)
{
     ProcessLine(scene, y);
}

Grâce à la TPL et sa méthode Parallel.For, la parallélisation est simplissime:

Parallel.For(0, ScreenHeight, y => ProcessLine(scene, y));

Du coup, automatiquement les lignes sont traitées en parallèle.

En termes de performances pour calculer l’image ci-dessus, cela donne:

TPL sur 8 cœurs

38s

Sans TPL (1 seul cœur donc)

89s

Une seule instruction permet un gain de 234%.

Au passage, de nombreux outils sont disponibles pour bien s’intégrer dans une application (je vous renvoie d’ailleurs à ma session sur les interfaces réactives des TechDays (teasing teasing!!)).

Ainsi l’appel principal du raytracer ressemble à ceci:

Task task = Task.Factory.StartNew(() =>
                 {
                   Parallel.For(0, ScreenHeight, y => ProcessLine(scene, y));
                 });
task.ContinueWith(t =>
                 {
                   if (OnAfterRender != null)
                            OnAfterRender(this, EventArgs.Empty);
}, TaskScheduler.FromCurrentSynchronizationContext());

On voit ici l’utilisation de la classe Task qui permet de lancer notre traitement en asynchrone tout en rajoutant un comportement dès que la tâche sera finie avec la méthode ContinueWith. De plus, ContinueWith à l’énorme avantage de pouvoir préciser le contexte de synchronisation. Ainsi nous pouvons faire en sorte qu’une fois notre tâche terminée, un événement soit appelé sur le thread principal et sans passer par le Dispatcher.

En gros, si j’avais voulu la jouer à l’ancienne avec par exemple le ThreadPool, cela aurait donné le code suivant:

ThreadPool.QueueUserWorkItem(o =>
                {
                    Parallel.For(0, ScreenHeight, y => ProcessLine(scene, y));
                    SynchronizationContext.Current.Post(state=> 
                                    {
                                        if (OnAfterRender != null)
                                            OnAfterRender(this, EventArgs.Empty);                         
                                    }, null);
                }, null);
En ce qui concerne le code complet, je le mettrai sur Codeplex dès que j’aurai fini l’intégration des formes non géométriques (à base de meshs donc).

TPL, .Net