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

C’est ma fore ?

17. novembre 2010

Ah!ah! Mon dieu que c’est drôle. Donc le sujet du jour est l’usage du sémaphore.

Le problème à la base est le suivant: J’utilise la TPL (Task Parallel Libray) de .NET pour effectuer des requêtes WEB en parallèle. Le code en question donne donc:

foreach (IHTMLElement el in (IHTMLElementCollection)document.body.all)
{
    if (el.tagName == "A")
    {
        string uri = el.getAttribute("href").ToString();
        tasks.Add(Task.Factory.StartNew(() => Process(uri)));
    }
}

Task.WaitAll(tasks.ToArray());

Dans la méthode Process, il y a principalement l’appel à une URI pour récupérer une image:

HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
WebResponse webResponse = webRequest.GetResponse();

Grâce à la TPL, mon code va donc être exécuté et schedulé par le framework et je vais pouvoir profiter à fond de tous les coeurs de mon ordinateur (ordinateur de bourgeois, il faut bien l’admettre car il contient 8 coeurs).

Notons au passage que j’ai ici fait le choix de faire explicitement des tasks mais que j’aurai pu écrire le même code sans les tasks en utilisant Parallel.ForEach.

 

Toutefois, il y a un hic. En effet, le site que je requête ne permet que 4 connexions en simultanée en provenance d’une même adresse IP. Or, ici je vais avoir de nombreuses tâches qui vont se lancer et qui vont donc générer de nombreuses connexions web. Le problème c’est que les quatre premières vont bien passer mais les suivantes vont être mises en attente par le serveur et risqueront très probablement de partir en timeout.

Il faut donc que je contrôle mes tasks (ou mon Parallel.ForEach) pour que les ordres ne soit pas plus de 4 à s’exécuter à la fois.

On pourrait bien sûr (comme je le vois si souvent sur les superbes architectures que je croise un peu partout) monter une usine à gaz monstrueuse qui s’occuperait de séquencer les jobs, de mettre en attente les envois de tasks, etc..

Mais on pourrait aussi être pragmatique et utiliser les outils à notre disposition. Et c’est là qu’entre en jeu le sémaphore (tadammmm!!!).

Ce dernier (et ceci depuis la nuit des temps, au moins) permet de contrôler l’accès à des ressources en nombre limité. Et c’est exactement ce que j’ai ici : je ne peux avoir que 4 requêtes en simultanées. Je vais donc juste déclarer un sémaphore ayant 4 ressources:

Semaphore semaphore = new Semaphore(4, 4);

Puis dans ma méthode Process (celle donc qui est exécutée par les tasks), je vais juste rajouter deux lignes (une tout au début et une tout à la fin) qui auront pour but de contrôler l’accès à la ressource:

semaphore.WaitOne();
...
semaphore.Release();

Ainsi, les 4 premières tasks passeront sans problème la barrière du WaitOne (qui décrémentera en interne le nombre de ressources disponibles) et à partir de la 5ème task, l’appel sera bloqué (de manière passive bien sur).

Dès qu’une task arrive sur le Release, elle libérera une ressource débloquant par la même une task en attente. Ainsi j’ai bien la garantie que mes requêtes web ne partiront pas en timeout et ceci avec un impact minimal Sourire

Donc vive TPL et les sémaphores!

.Net, TPL