posté @ Tuesday, June 02, 2009 10:18 AM

La beta 1 de Visual Studio 2010 et du Framework .Net 4.0 est disponible depuis quelques jours (normalement, si vous lisez ce blog, vous êtes déjà au courrant !), et une des nouveautés majeures de cette nouvelle version (si ce n’est LA nouveauté) concerne le Multi-Threading. En effet, .Net 4.0 intègre un certain nombre d’améliorations au niveau du ThreadPool qui ont permis de créer tout un jeu d’APIs dédiées à l’exécution de tâches en parrallèle et à leur synchronisation.

Ces APIs se distinguent en 3 catégories :

  • Task & TaskScheduler : Il s’agit de la possibilité de créer des tâches avec une granularité très fine, de définir des dépendances (telle tâche ne peut démarrer avant la fin de l’exécution de telle autre), et de les exécuter. Le TaskScheduler se chargera alors de distribuer ces tâches entre les différents Threads du ThreadPool pour exploiter au mieux les ressources de la machine. On peut aussi utiliser les tâches pour effectuer des traitements asynchrones afin de ne pas bloquer un Thread de rendu en WPF ou Windows Forms par exemple.
  • Parallel loops : Il s’agit d’un ensemble de méthodes statiques remplacant les boucles traditionelles, permettant de traiter chaque itération d’une boucle for / foreach dans des tâches séparées afin de parallèliser le traitement, et potentiellement d’aggréger les résultats des traitements de chaque tour de boucle. Parallel.For, Parallel.Foreach, Parallel.Do etc. reposent sur les classes Task et TaskScheduler.
  • Parallel Linq : Il s’agit d’une extension à Linq to Objects permettant de paralleliser le traitement d’une requête. Si vous aimé tout comme moi le style de développement fonctionnel apporté par C# 3.0 et que vous mettez des lambdas, et des appels à Enumerable.Where, Enumerable.Select, etc. partout dans votre code, vous adorerez PLinq :).

L’exemple qui vient avec cet article exploite essentiellement les parallel loops. Le but de l’application est d’effectuer des traitements sur une série d’images (ajustement de contraste). L’algorythme employé n’est pas le plus optimisé qui soit, le but étant de mettre en valeur la simplicité des APIs plutôt que de concurrencer Photoshop. L’exemple a aussi l’avantage de montrer quelques astuces spécifiques au développement multi-thread avec WPF.

Focus 1 : chargement des images

La première utilisation que je fais de PTL, est assez classique, et équivaut à l’utilisation d’un BackgroundWorker. Il s’agit simplement de charger les images dans un thread séparé pour éviter de bloquer le thread UI de WPF :

if (ofd.ShowDialog() ?? false)
{
    _images.Clear();
    Task.Factory.StartNew(() =>
        {
            foreach (var file in ofd.FileNames)
            {
                try
                {
                    var source = new BitmapImage(new Uri(file));
                    source.Freeze();
                    var tuple = new SourceAndTarget { Source = source };
                    Dispatcher.BeginInvoke((Action)(() =>
                        {
                            _images.Add(tuple);
                            tuple.Target = new WriteableBitmap(source);
                        }));
                }
                catch
                {
                    MessageBox.Show("Image " + file + " failed");
                }
            }
        }).ContinueWith((t) =>
            Dispatcher.BeginInvoke((Action)(() => LoadingImages = false)));
}

Task.Factory.StartNew permet de créer une tâche à partir d’un délégué (ici une lambda) et de la scheduler. Notez la ligne source.Freeze(), elle est très importantes. En effet, avec WPF, tout DependencyObject ne peut par défaut être manipulé que dans le thread où il a été créé. Hors, comme on est dans un thread différent du Thread UI, la BitmapImage que l’on vient de créer ne peut pas, par défaut être accessible par le Thread UI.

Il existe dans WPF une classe dérivant de DependencyObject appelée Freezable permettant de “figer” l’objet et le rendre immutable. Une fois que l’objet est figé, il n’y a plus de risques liés à la concurrence d’accès (car tout accès en écriture aux propriétés est désactivé), l’objet devient accessible depuis n’importe quel thread. Beaucoup d’objets (notament les images, les brushes, les Paths etc) de WPF héritent de cette classe Freezable. Une fois notre BitmapImage crée, nous appelons donc sa méthode Freeze() pour la rendre accessible au thread UI de WPF.

Le cas de la WriteableBitmap est plus complexe, car elle demande d’être accessible par le thread UI tout en restant modifiable. Grace au Dispatcher, nous basculons donc dans le Thread UI et créons la WriteableBitmap depuis celui-ci.

Focus 2 : traitement en parallèle avec Parallel.For

Le traitement d’image se prête assez facilement à la parallèlisation. En effet, il suffit de découper l’image source, calculer les différentes portions dans des threads séparés et recomposer l’image destination. La classe ImageProcessor fournie dans l’exemple illustre cela parfaitement : que l’on passe en parallèle ou que l’on reste en traitement séquenciel, la grosse majorité du code reste identique. Seulement dans un cas, on parallèlise la boucle de traitement :

public void ProcessSequential(WriteableBitmap target, ITransformer transformation)
{
    Contract.Requires(target != null);
    Contract.Requires(transformation != null);
    int count = Math.Min(Environment.ProcessorCount * 2, _image.PixelWidth);
    int chunkWidth = _image.PixelWidth / count;

    for (int i = 0; i < count; i++)
    {
        ProcessChunck(target, transformation, count, chunkWidth, i);
    }
}
public void ProcessParallel(WriteableBitmap target, ITransformer transformation)
{
    Contract.Requires(target != null);
    Contract.Requires(transformation != null);
    int count = Math.Min(Environment.ProcessorCount * 2, _image.PixelWidth);
    int chunkWidth = _image.PixelWidth / count;

    Parallel.For(0, count, (i) =>
    {
        ProcessChunck(target, transformation, count, chunkWidth, i);
    });
    
}

Le nombre de “morceaux” d’image générés est lié au nombre de processeurs. Le *2 est simplement là pour augmenter la granularité car on ne sait pas si l’algorythme de transformation prendra autant de temps pour chaque morceau d’image. Le contenu de la méthode ProcessChunk n’est quand à lui pas très compliqué. Il s’agit simplement de récupérer les données de l’image source correspondant au morceau à traiter, de le traiter, et de l’injecter dans la WritableBitmap. On aura d’ailleurs besoin de se replacer dans le Thread UI pour effectuer cette dernière opération (la seule à ne pas être parallèlisée) :

private void ProcessChunck(WriteableBitmap target, ITransformer transformation, int count, int chunkWidth, int i)
{
    var width = chunkWidth;
    if (i == count - 1)
        width += _image.PixelWidth % chunkWidth;

    var rect = new Int32Rect(i * chunkWidth, 0, width, _image.PixelHeight);
    int stride = width * 4;
    byte[] data = new byte[stride * _image.PixelHeight];
    _image.CopyPixels(rect, data, stride, 0);
    var transformed = transformation.Transform(new ImageChunk(rect, data));
    target.Dispatcher.BeginInvoke((Action)(() =>
    {
        target.WritePixels(transformed.Rect, transformed.Data, stride, 0);
    }));
}

Conclusion

Parallel Task Library apporte une réelle facilité de syntaxe pour le développement d’applications faisant appel au parallèlisme (que ce soit pour effectuer un traitement asynchrone ou pour exploiter la puissance d’un processeur multi-coeur). En plus de cela, Visual Studio 2010 apporte une collection d’outils permettant de faciliter le Debugging d’applications multi-thread, ainsi qu’un outil de profiling permettant de détecter les problèmes rencontrés dans ces applications (contentions, attente sur une ressource partagées…).

Ce qu’il ne faut par contre pas oublier, c’est que quelque soit la technologie employée (PTL, OpenMP, etc…), il ne s’agit que d’outils simplifiant les aspects technique de la création d’applications Multi-Thread. En effet, PTL ne dispense pas le développeur de “penser” ses algorythmes dans un style parallèle (découpage en tâches, détection des tâches non-dépendantes, utilisation d’objets immutables, éviter les partages d’états…). Il faudra donc de toute façon se méfier des Race conditions, des deadlocks, etc. Le style de programmation fonctionnel apporté par C# 3.0 se prète par contre très bien à la parallèlisation, car il pousse à l’utilisation d’objets immutables et donc évite le problème le plus courrant lors du développement parallèle : le partage d’état.

Sources de la solution : ici.

Mots clés Technorati : ,,

Commentaires :

# re: [.Net 4.0] Parallel Task Library
Ecrit par Bruno Boucard le 10/30/2009 9:59 AM
Bravo pour cette introduction à la programmation parallèle.

Bruno
# Buy vicodin online.
Ecrit par Vicodin overdose. le 7/16/2010 11:09 PM
Buy vicodin. Vicodin. Re your vicodin refill is ready. Vicodin no prescription. Vicodin without prescription. Addiction vicodin.

Ecrire un commentaire :

Titre :*
Nom *
Email
Url
Commentaire : *  


Please add 7 and 5 and type the answer here: