Overblog Suivre ce blog
Editer l'article Administration Créer mon blog
4 septembre 2013 3 04 /09 /septembre /2013 15:12

 

J'ai sorti hier une deuxième version alpha de Rossignol. Pour fêter cela, j'ai décidé de consacrer un autre petit billet à ce nouveau projet. Nous avions vu la dernière fois les choix que j'avais fait en matière de bibliothèque graphique et de parsing XML. Cette fois je vais vous parler de threading.

Garder une interface réactive

 

Dans un logiciel avec une interface graphique, la tâche principale de l'application est une boucle d'événements. L'application se contente d'attendre un message que lui envoie le gestionnaire de fenêtre (un événement), le traite et se remet en attente d'un message suivant. Selon le système, cette boucle de message peut prendre plusieurs formes, mais avec SWT, ça ressemble à ça :

 

while (!window.isDisposed())

{

  if (!display.readAndDispatch())

    display.sleep();

}

 

 

Si l'application met trop de temps à répondre à un événement, elle aura du retard pour traiter l'événement suivant. Les conséquences pour l'utilisateur en seront immédiates : l'interface du programme (ses fenêtres et boutons) ne s'actualisera plus pendant quelques temps. Elle ne réagira pas à aucune sollicitation. Elle sera comme figée et il semblera à l'utilisateur que le programme a planté, jusqu'à ce que le traitement sont terminé.

 

Pour pallier ce problème, une solution consiste à faire effectuer tous les traitements longs dans des threads séparés (on parle de « worker threads »). Ainsi, pendant que le thread principal continue de traiter les nouveaux messages qui lui arrivent, l'application peut effectuer des traitements longs. C'est pour cela que les ingénieurs de chez Microsoft, en concevant la nouvelle API Windows RT, ont imposé que toute opération pouvant prendre plus de 50μs se fasse dans un thread séparé. Même si je n'utilise pas leur API ni leur nouveau système d'exploitation, c'est une consigne que j'ai souhaité suivre pour la réalisation de Rossignol.

 

À l'issue de ces traitements, les worker threads ont souvent le besoin de modifier l'affichage de l'application, par exemple, pour afficher le résultat d'un calcul. Et c'est là que les choses se compliquent. La plupart des gestionnaires de fenêtre ne gèrent pas la modification concurrente de l'interface par des threads séparés. La plupart de bibliothèques imposent donc la contrainte suivante : toute modification de l'interface doit être faite dans le thread principal (celui qui s'occupe de la boucle de messages).

 

Chaque bibliothèque propose donc en général d'un mécanisme pour faire renvoyer du « travail à faire » d'un worker thread vers le thread principal. En SWT, ce sont les méthodes syncExec et asyncExec de la classe Display.

 

Tout ceci est fort joli, mais il s'agit d'une solution Java, un langage étranger à Rossignol (qui ne contient aucune ligne de Java). Voyons donc comment D gère les threads, dans tout ça…

 

 

Le threading en D

 

 

D est un langage moderne dont la première version stable est apparue après l'avènement des microprocesseurs multicœurs grand public. Il est a donc tout naturellement inclus les problématiques de gestion du multitâche dans son système de type. Les concepteurs de D ont fait plusieurs constats :

  • Les bugs liés au multithreading sont essentiellement dus à une mauvaise synchronisation des accès à des ressources partagées entre les threads.

  • Dans la plupart des langages, la mémoire est implicitement partagée entre les threads.

 

Dès lors, ils ont choisi d'inverser le problème en interdisant par défaut le partage de variable entre les threads :

  • Par défaut, les variables globales sont thread-local. Sauf si elles sont implicitement marquées shared (qui est un qualificateur de type).

  • Les constantes (objets immuables) sont implicitement partagées et n'ont pas besoin d'être synchronisées.

  • Les objets shared doivent faire l'objet de précautions particulières dans leur manipulation.

 

Le threading de bas niveau est implémenté dans DRuntime (modules core.*). C'est l'environnement d'exécution de D, qui est constitué de code spécifique à un système ou un compilateur donné. La classe core.thread.Thread en particulier se charge de créer un thread au moyen de sa méthode start. C'est le pendant de java.lang.Thread. C'est la solution à utiliser à bas niveau, si l'on veut avoir le maximum de contrôle ou si l'on doit écrire du code pas vraiment idiomatique, comme par exemple utiliser certaines fonctionnalités écrites à l'origine pour un autre langage.

 

Au-dessus de ceci, la bibliothèque standard (modules std.*) propose deux modules distincts.

 

Le premier s'appelle std.concurrency. C'est historiquement le premier à avoir été implémenté. Il propose un système de messagerie entre threads. Le module se charge de vérifier à la compilation que les threads ne puissent s'échanger que :

  • Des types valeurs

  • Des types références thread-safe (classes synchronized ou immutable par exemple).

 

Ces contraintes permettent de garantir que l'accès à des ressources partagées est explicite et contrôlé. Ce module est utile lorsqu'un nombre défini de threads hétérogènes ont des gros besoins de communication. C'est encore malgré tout un module d'assez bas niveau puisque la création de threads est explicite et contrôlée par l'utilisateur.

 

Le second module s'appelle std.parallelism. Celui-ci est basé sur des primitives de haut niveau : foreach parallèle, map et reduce parallèles, ainsi qu'un concept de tâches similaires aux futures et promises de C++11.

 

Si vous avez l'expérience du package java.util.concurrent de Java 5 ou de Grand Central Dispatch de Objective-C, vous serez également en terrain connu : la classe TaskPool se comporte de manière similaire à Executor de Java ou aux dispatch queues d'Apple : c'est une FIFO à laquelle on soumet des tâches à exécuter qui seront dispatchées automatiquement sur un nombre donné de threads.

 

Ce module est donc approprié lorsque la même opération doit être exécutée en parallèle sur des données différentes, ou qu'une opération doit être effectuée en tâche de fond avant d'être retournée à un thread bien spécifique.

 

 

Et Rossignol dans tout ça ?

 

Si je ne vous ai pas déjà perdus, vous aurez compris de quelles solutions on a besoin en développant une application DWT :

  • Pour toute tâche où l'on aurait besoin d'un java.lang.Thread ou si l'on doit contourner le système de type pour utiliser SWT, il faut utiliser core.thread.Thread.

  • Pour le reste, quand on a besoin d'un traitement long à lancer en tâche de fond, std.parallelism et ses classes Task et TaskPool font merveille.

 

Partager cet article

Repost 0
Published by Olivier - dans Projets personnels
commenter cet article

commentaires

Présentation

Recherche

Liens