Introduction
Aujourd’hui, avec la multiplication des cœurs, il serait regrettable de ne pas optimiser ses algorithmes pour permettre d’utiliser tous les cœurs disponibles et bien entendu d’augmenter leurs performances. La programmation parallèle, ou le multithreading, est de base difficile à maitriser et à mettre en place, même avec le Framework .NET. A l'instar d'OpenMP pour C++, Microsoft propose depuis quelques mois les Parallel Extensions (plus communément appelé « PFX » chez nos amis anglo-saxons, qui vient de « ParallelFX »).
Les Parallel Extensions reprennent le principe du multithreading, tout en étant « quasi-transparent » aux yeux du développeur, cela limite ainsi le risque d’erreur et augmente les performances des applications, tout cela sans grand changement dans votre manière de développer. Alors que la programmation parallèle « classique » est lourde à mettre en place et change votre manière de développer.
Cet article est une introduction aux Parallel Extensions. Ces extensions sont disponibles gratuitement en version CTP (Community Technology Preview) ; et la version finale sera intégrée avec le Framework .NET 4.0, attendue en même temps que la sortie de la nouvelle version de Microsoft Visual Studio 2010. A l'heure o�� ces lignes sont écrites, une CTP de Visual Studio 2010 est disponible sur le site de Microsoft en libre téléchargement.
Rappels (définitions)
Processus : un processus est un programme en cours d'exécution. Chaque processus sur un système possède sa propre zone mémoire protégé, c'est-à-dire qu'en théorie, ce processus est le seul à pouvoir accéder à cette zone mémoire.
Processus léger (ou thread) : un thread est une entité indépendante qui résulte de la décomposition d'un processus. A l'œil de l'utilisateur, les threads sont exécutés « simultanément », or c'est faux. Comme le montre le schéma ci-dessous, un thread s'exécute un temps donné, puis donne la main à un second thread, puis reprends la main... et ainsi de suite : un processeur ne peut effectuer qu'une seule opération à la fois. Les threads partagent la même mémoire que les processus.
Par exemple, un thread permet à l'utilisateur de travailler sur un programme sans être bloqué alors que celui-ci effectue une opération en tâche de fond (sauvegarde automatique, vérification d'un fichier...etc).
Schématisation (pour monocore) :
Programmation parallèle (ou multithreading) : la programmation parallèle permet d'exécuter simultanément plusieurs actions sur les cœurs disponibles sur la machine. En fait, avec la programmation concurrente, le système d'exploitation décide sur quel cœur affecter telle ou telle action ; mais dans un développement parallèle, on va « indiquer » au système d'exploitation la répartition des actions sur les cœurs.
Schématisation (pour multicore) :
Pourquoi la programmation parallèle avec PFX ?
Se poser la question de la différence entre la programmation concurrente et de la programmation parallèle est évidente. On pourrait croire que c'est la même chose, or il en est tout autre.
La programmation concurrente est source de plusieurs inconvénients :
- Lisibilité du code et donc de maintenance ;
- Débogage ;
- Une discipline certaine dans la structure de votre code, qui n'est pas au goût de tous ;
- La pseudo-simultanéité du code (voir schémas précédent).
Ce dernier point est en particulier à l'avantage de la programmation parallèle : les actions sont exécutés simultanément. Donc c'est un avantage certain sur les performances si, bien sûr, c’est bien utilisé.
Avec PFX, Microsoft permet de faire de la programmation parallèle très facilement. Nous n'avons pas besoin d'être des experts en programmation parallèle pour se faire, car le code qui en découle est accessible à n'importe quel développeur.
Aussi, la lisibilité du code, et donc la maintenance qui en découle, est améliorée face à la programmation concurrente. Donc la complexité est considérablement réduite.
Comment utiliser PFX ?
Les exemples suivants sont à but didactique, il se peut que suivant votre configuration, le code séquentiel soit plus performant que le code « PFX ». D'ailleurs, pour vos développements, il est intéressant de noter que vous pouvez mesurer les performances de vos applications grâce à la classe Stopwatch.
Il faut aussi préciser que même si vous ne disposez pas d'un multicore, PFX fonctionnera sur du monocore, et donc nous pouvons l'utiliser dans nos projets sans à avoir à modifier quoique ce soit pour un parc donné.
Dans les exemples pour PFX qui vont suivre, la collection BlockingCollection sera utilisée (espace de nom System.Threading.Collections), le résultat est identique que la collection List mais dans un contexte thread-safe (il existe aussi ConcurrentQueue et ConcurrentStack).
Pour commencer, il faut se rendre sur cette page, afin de télécharger la dernière version CTP (Juin 2008). Pour pouvoir l’utiliser, il faut ajouter la référence « System.Threading » au projet.
Pratique
Dans les deux prochains exemples, nous allons comparer le code PFX et lecode que l'on a l'habitude d'utiliser. Nous verrons que l'implémentation de PFX est très aisé.
- Exemple avec for
PFX
BlockingCollection list = new BlockingCollection();
Parallel.For(0, 1000, (i) =>
{
list.Add(i);
Trace.WriteLine("i = " + i.ToString() + " | thread id = "
+ Thread.CurrentThread.ManagedThreadId.ToString());
}
// Résultat à l'exécution (sortie)
// i = 791 | thread id = 14
// i = 792 | thread id = 14
// i = 799 | thread id = 6
// i = 800 | thread id = 6
Séquentiel
List list = new List();
for (int i = 0; i < 1000; i++)
{
list.Add(i);
Trace.WriteLine("i = " + i.ToString() + " | thread id = "
+ Thread.CurrentThread.ManagedThreadId.ToString());
}
// Résultat à l'exécution (sortie)
// i = 798 | thread id = 10
// i = 799 | thread id = 10
// i = 800 | thread id = 10
// i = 801 | thread id = 10
Après l’exécution de ces exemples, on peut constater qu’avec PFX, la boucle for est effectuée dans plusieurs threads, alors que sous forme séquentielle, tout se fait dans un seul et unique thread.
On peut voir ici que le code reste relativement simple à abordé avec PFX : les threads sont gérés automatiquement par le Framework, et ils seront « distribués » sur plusieurs cœurs. On peut noter aussi que les valeurs de la variable i ne se suivent pas comme sous forme séquentielle, c'est tout simplement du au fait que le code est exécuté sur plusieurs cœurs à la fois.
Voyons maintenant comment fonctionne les foreach, reprenons l’exemple précédent, en parcourant tous les éléments de la collection :
- Exemple avec foreach
Code PFX
BlockingCollection<int> list = new BlockingCollection<int>();
Parallel.For(0, 1000, (i) =>
{
list.Add(i);
});
Parallel.ForEach(list, (i) =>
{
Trace.WriteLine("i = " + i.ToString() + " | thread id = "
+ Thread.CurrentThread.ManagedThreadId.ToString());
});
// Résultat à l'exécution (sortie)
// i = 815 | thread id = 12
// i = 816 | thread id = 12
// i = 800 | thread id = 11
// i = 801 | thread id = 11
Code Séquentiel
List list = new List();
for(int i = 0; i < 1000; i++)
{
list.Add(i);
}
foreach(int i in list)
{
Trace.WriteLine("i = " + i.ToString() + " | thread id = "
+ Thread.CurrentThread.ManagedThreadId.ToString());
}
// Résultat à l'exécution (sortie)
// i = 798 | thread id = 10
// i = 799 | thread id = 10
// i = 800 | thread id = 10
// i = 801 | thread id = 10
Maintenant, voyons comment exécuter des méthodes asynchrones (peut exécuter plusieurs actions indépendantes, sans attendre que les précédentes soient terminées) avec PFX.
Parallel.Invoke(() => ShowString("TEST 1"),
() => ShowString("TEST 2"),
() => ShowString("TEST 3"),
() => ShowString("TEST 4"),
() => ShowString("TEST 5"));
static void ShowString(string value)
{
Console.WriteLine("Thread ID : "
+ Thread.CurrentThread.ManagedThreadId.ToString()
+ " - " + value);
}
// Résultat à l'exécution
// Thread ID : 11 - TEST 1
// Thread ID : 14 - TEST 3
// Thread ID : 12 - TEST 2
// Thread ID : 13 - TEST 4
// Thread ID : 11 - TEST 5
Cet exemple est vraiment très simple et se situe loin de la réalité, mais il a pour but de vous montrer la facilité d'utilisation. Pour utiliser plus « finement » les méthodes asynchrones, il existe les classes Task et Future (dans l'espace de nom System.Threading.Tasks).
Pour finir, voyons comment cela se passe au niveau de LINQ (Language-Integrated Query), renommé PLINQ pour son utilisation parallèle (Parallel Language-Integrated Query).
Exemple avec PLINQ
Code PFX
int[] list = {10,11,5,4,2,1,9,21,47,58,78,48,54,62,31,20,14,03,16};
var test = from item in list.AsParallel()
orderby item
select item;
foreach (int i in test)
{
Trace.WriteLine(i.ToString());
}
// Résultat à l'exécution (sortie)
// 1
// 2
// 3
// 4
// 5
Code séquentiel
int[] list = {10,11,5,4,2,1,9,21,47,58,78,48,54,62,31,20,14,03,16};
var test = from item in list
orderby item
select item;
foreach (int i in test)
{
Trace.WriteLine(i.ToString());
}
// Résultat à l'exécution (sortie)
// 1
// 2
// 3
// 4
// 5
Pour paralléliser vos requêtes, il faut tout simplement ajouter la méthode d'extension AsParallel() au tableau list, comme le montre l'exemple.
Conclusion
Cet article est une introduction à PFX : comme vous aurez pu le comprendre, les capacités de PFX sont assez vastes pour pouvoir les traiter entièrement. Je vous conseille donc de regarder la documentation MSDN, ou celle disponible sur sa page de téléchargement.
Malgré le fait que PFX est très intéressant car il simplifie grandement le développement de logiciel pour plusieurs cœurs, il ne faut pas oublier qu'il ne faut pas en abuser, sinon nous pouvons avoir dans certains cas une dégradation des performances.
Liens utiles :
- http://blogs.msdn.com/devpara/ : Blog en français officiel dédié à PFX
- http://blogs.msdn.com/pfxteam/ : Blog de la team de PFX
- http://msdn.microsoft.com/en-us/concurrency/default.aspx : Portail officiel
Vous pouvez aussi retrouver cet article sur labo-dotnet.com (edit : lien mort).