If THen ELse…

journal de bord d'un aspirant codeur

Dernièrement, j’ai été amené à développer en Java un système de gestion de comptes utilisateurs pour l’université dans laquelle j’étudie. Une fois de plus, j’ai du faire face à la nécessité de gérer la communication entre les différents modules composant mon application. Et une fois de plus, j’ai perdu patience.

L’interface n’est pas très complexe, mais la modularité des composants graphiques apporte une difficulté concernant la transmission des données des uns aux autres. Autrement dit c’est un bordel monstre.
Jusqu’à présent je n’avais pas trouvé de solution esthétique et simple à implémenter qui permettait à tous mes modules (en l’occurence des JPanel) de correspondre les uns avec les autres. Parmi les solutions disponibles mais, à mon sens, peu efficaces, notons:

  • l’utilisation des classes privées. Principal défaut: vous vous retrouvez avec des fichiers sources illisibles. Au delà de deux classes déclarées dans un même fichier on commence à s’y perdre sérieusement. L’autre défaut est que vous ne pouvez pas réutiliser vos composants ailleurs…
  • l’utilisation du design pattern observateur. Parfait quand une classe doit correspondre avec une autre et que vous pouvez accéder aux deux au même endroit pour créer la liaison. Le problème dans mon cas était que A devait communiquer avec D, qui était déclaré dans C, lequel était lui même déclaré dans B. Au final ça revenait à peu près au même que d’utiliser…
  • la dernière solution, qui consiste à fait en sorte de pouvoir traverser toute la chaîne des composants, des parents aux enfants. Et ça c’est moche.

J’ai donc décidé de coder un petit système de communication reprenant le concept des signaux utilisés notamment en ActionScript.

Présentation du concept.
L’idée consiste à créer un objet accessible par tous au moyen d’une méthode statique, un singleton que nous appellerons Dispatcher, et qui serait chargé de centraliser la gestion de la communication entre les divers objets à travers l’utilisation de signaux. L’accès au dispatcher est libre: n’importe quel objet peut envoyer et écouter n’importe quel signal.

Un signal est en fait un simple String, et les objets désirant écouter des signaux doivent implémenter une interface, DispatcherListener, et s’enregistrer auprès du dispatcher.

Par la suite, le fonctionnement du système suit la logique suivante:

  1. Un objet demande au dispatcher d’envoyer, par exemple, le signal « S1″ couplé à un Object contenant une donnée quelconque.
  2. À l’intérieur du dispatcher, un objet SignalEvent encapsulant le nom du signal, l’Object associé et la source de l’émission est crée.
  3. Le dispatcher récupère la liste des écouteurs enregistrés pour ce signal.
  4. Chacun des écouteur est notifié grâce à l’appel de la méthode signalReceived(SignalEvent)

schémas

Application
Nous devons créer deux classes et une interface:

  • L’interface DispatcherListener qui nous permettra d’indiquer aux écouteurs la méthode qu’ils doivent implémenter pour que le dispatcher puisse les notifier.
  • La classe SignalEvent qui encapsule les données relatives au signal qui a été envoyé.
  • Enfin, notre singleton Dispatcher qui gérera l’acheminement des signaux ainsi que l’enregistrement et la suppression des écouteurs.
    J’ai utilisé la structure de données HashMap pour associer les signaux aux liste d’écouteurs: les clés sont les signaux et les données sont des ArrayList. À l’origine je voulais placer les écouteurs dans des WeakReference, mais j’ai dû abandonner l’idée quand j’ai réalisé que cette décision m’empêchait de créer les écouteurs à l’aide de classes anonymes (ce qui explique au passage pourquoi les développeurs de swing n’ont pas retenu cette option). Quoiqu’il en soit, les fonctions d’accès et de modification sont toutes synchronisées pour éviter les incohérences éventuelles dues à la manipulation du dispatcher par plusieurs threads en même temps.

Les sources sont disponibles ici.

Objectif: affecter un certain nombre d’attributs supplémentaires à une classe X ne pouvant être étendue.
Solution: encapsuler les informations complémentaires dans une classe et réaliser l’association avec les instances de X à l’aide d’une table de hachage.

Le garbage collector est le processus responsable de la désallocation mémoire des objets non utilisés. Comme nous allons le voir, une bonne connaissance de son fonctionnement est nécessaire pour qui veut réaliser une application qui ne monopolise pas 100% des ressources système après 10 minutes d’utilisation…
Le garbage collector utilise deux méthodes pour savoir si un objet est actif ou non: le compteur de référence (reference counting) et les marques de balayage (mark sweeping).

  • Compteur de référence.
    Chaque objet possède un compteur dont le rôle est d’indiquer le nombre de références associées. Lorsque le compteur indique 0, c’est le signe que plus aucune référence ne pointe vers l’objet en question et qu’il peut donc être libéré de la mémoire. Ce système est simple à mettre en place et fonctionne bien la plupart du temps, sauf dans les cas de références circulaires (a pointe vers b qui pointes vers a). À partir de la version 8 du Flash Player, ce type de situation n’échappe plus au garbage collector grâce à la technique du mark sweeping.
  • Mark sweeping.
    La machine virtuelle part de la racine de l’application et parcourt de manière récursive tous les objets qui lui sont liés, directement ou indirectement. Elle recense ainsi tous les objets actifs. Une fois l’opération terminée, elle peut donc déduire que les objets non recensés sont inactifs et peuvent être libérés.

L’ennui, c’est qu’il est impossible de savoir exactement quand le garbage collector libérera de la mémoire les objets inactifs. Ce processus réalise en effet un certain nombre de vérifications heuristiques (allocation de la mémoire vive, état de la pile entre autres choses) pour déterminer quand le nettoyage doit avoir lieu. La conséquence directe de ce mode de fonctionnement est que des objets inactifs depuis un certain temps peuvent continuer à utiliser les ressources de votre système, comme le montre cette animation, et vous n’avez aucun moyen pour forcer leur suppression.

De plus, supprimer les références pointant vers un objet donné semble plus facile à dire qu’il n’y parait car, dans la pratique, de très nombreuses références vers les objets de votre application sont créées de manière transparente, lorsque vous mettez en place des écouteurs par exemple. Et de fait, les écouteurs sont la principale cause de fuite mémoire, c’est à dire d’espace mémoire gaspillé par des objets dont la logique voudrait qu’ils ne soient plus référencés (plus d’information sur le le modèle événementiel d’ActionScript ici). Avant de supprimer un objet, il faudra donc s’assurer que ce dernier ne joue pas le rôle d’écouteur.
Il convient d’insister car beaucoup de fausses informations sur le sujet circulent sur internet: lorsque vous déclarez un écouteur, c’est bien le diffuseur qui possède une référence vers l’écouteur et non l’inverse. Autrement dit dans le code suivant:

var monObjetPerso:ClassePerso = new ClassePerso();
var monClip:MovieClip = new MovieClip();
 
monClip.addEventListener( Event.ENTER_FRAME, monObjetPerso.ecouteur );
monObjetPerso = null;  //tentative de libération de la mémoire

Ici, malgré la libération explicite, l’objet monObjetPerso restera en mémoire parce que l’objet monClip l’a référencé dans sa liste des écouteurs de l’évènement ENTER_FRAME. Pour bien faire il aurait fallu auparavant appeler:

monClip.removeEventListener(Event.ENTER_FRAME, monObjetPerso.ecouteur);

Il existe une alternative permettant de passer outre la suppression manuelle des écouteurs: avec la version 3 d’ActionScript est apparu le concept de référence faible (weak reference) permettant d’indiquer à la machine virtuelle que la référence générée lors de l’ajout d’un écouteur ne doit pas être prise en compte par le garbage collector. Le code suivant n’induira donc pas de fuite de mémoire:

var monObjetPerso:ClassePerso = new ClassePerso();
var monClip:MovieClip = new MovieClip();
 
// creation de l'ecouteur. Les paramètres 3 et 4 sont les paramètres par défaut, le dernier paramètre est passé à true pour indiquer une référence faible
monClip.addEventListener( Event.ENTER_FRAME, monObjetPerso.ecouteur, false, 0, true );
monObjetPerso = null;

Conclusion
La détection des fuites de mémoire est une tâche longue et pénible à réaliser. Deux outils permettent toutefois de faciliter cette opération: le profiler disponible dans la version Premium du Flash Builder et le module libre KapInspect.

Mes sources: ici, ici et .

J’ai évoqué dans un précédent article les différents modes de verrouillage communément utilisés par les SGBD. Je ne compte bien entendu pas épuiser toute la question tant il semble qu’il y ait de choses à dire à ce sujet, mais j’ai dernièrement fait une petite découverte qui mérite d’être exposée ici. Je me suis en effet demandé ce qu’il se passait lorsque, au cours d’une même transaction, certaines lignes étaient d’abord verrouillées à l’aide d’un verrou partagé puis d’un verrou exclusif (et inversement). Quel verrou le système va-t-il conserver? Le premier? Le dernier à avoir été mis en place? Ou bien le verrou le plus restrictif?

Premier essai: sélection des lignes d’une table en mode partagé puis exclusif.

START TRANSACTION;			 |
SELECT * FROM tableX LOCK IN SHARE MODE; |
// Affichage des lignes sélectionnées	 |
SELECT * FROM tableX FOR UPDATE;	 |
// Affichage des lignes sélectionnées	 |
					 | START TRANSACTION;
					 | SELECT * FROM tableX LOCK IN SHARE MODE;
					 | // En attente de l'obtention du verrou

Analyse:
c’est de toute évidence le dernier verrou posé par la première transaction qui est pris en compte, autrement la seconde ne serait pas bloquée pour réaliser sa sélection.
Il faut maintenant déterminer si le verrou a été choisi parce qu’il a été posé le dernier ou bien parce qu’il est plus restrictif que le premier.

Second essai: sélection des lignes d’une table en mode exclusif puis partagé.

START TRANSACTION;			 |
SELECT * FROM tableX FOR UPDATE; 	 |
// Affichage des lignes sélectionnées	 |
SELECT * FROM tableX LOCK IN SHARE MODE; |
// Affichage des lignes sélectionnées	 |
					 | START TRANSACTION;
					 | SELECT * FROM tableX LOCK IN SHARE MODE;
					 | // En attente de l'obtention du verrou

Conclusion:
c’est désormais clair, le système prendra toujours en compte le verrou le plus restrictif posé durant la transaction. C’est un élément important à prendre en compte dans des applications où des accès sont réalisés sur les même tables par différents modules indépendants: il n’y a aucun risque de mauvaise gestion de la concurrence si à un moment ou un autre un verrou partagé est déposé sur une ligne auparavant verrouillée de manière exclusive.

Le sujet qui m’intéresse directement ici est celui des requêtes préparées. Les points forts des requêtes préparées sont de deux ordres:

  • d’une part la sécurité de votre application est accrue: adieu les injections SQL.
  • ensuite l’exécution de requêtes identiques est accélérée.

Modalités d’exécution des requêtes par les SGBD.
Quand un Systèmes de Gestion de Bases de Données (SGBD) reçoit une requête, il lui fait subir deux types d’analyse: le système s’assure d’abord que la requête ne comporte pas d’erreur, qu’elle est bien valide. Ensuite il tente d’établir le moyen optimal de l’exécuter en prenant en compte les index qui ont été définis dans les tables concernées. Cette dernière opération peut être très longue; cela dépend bien sûr de la complexité des requêtes que vous générez, du nombre de tables concernées, des index que vous avez défini, judicieusement ou non, etc, mais pour la plupart des applications il n’est pas acceptable qu’une requête mette plus d’une seconde pour s’exécuter.

La plupart des SGBD utilisent un système de cache pour éviter de devoir analyser plusieurs fois les mêmes requêtes. Ce système repose sur une table de hachage: le contenu de la requête sert de clé, et la clé mène au plan d’exécution. L’ennui, c’est que le plus petit changement dans une requête aura pour conséquence de générer une clé différente et sera ainsi perçu comme une nouvelle requête par le SGBD:
Ainsi les plans d’exécution de

SELECT * FROM tableX WHERE tableX.champ > 10

et de

SELECT * FROM tableX WHERE tableX.champ > 20

sont identiques, et pourtant le SGBD lancera une analyse pour chacune d’entre elles. La solution: utiliser des requêtes préparées:

SELECT * FROM tableX WHERE tableX.champ > ?

Se faisant vous pouvez remplacer la variable par la valeur que vous désirez APRES que la clé ait été générée et le plan d’exécution établi.

Pool de connexions et requêtes préparées.
Malheureusement, les requêtes préparées sont associées aux connexions et disparaissent avec elles. Ce qui serait idéal, ce serait de pouvoir créer une fois pour toute l’ensemble des requêtes préparées voulues afin de les mettre à disposition de tous les clients durant toute la durée de vie du serveur. Les concepteurs de J2EE y ont pensé, et ils ont intégré cette possibilité de manière transparente aux pool de connexions en associant un cache à chacune des connexions du pool (je ne parlerai pas ici de la création des pool de connexion avec Tomcat: Christophe Jollivet sur developpez.com s’en est déjà chargé, et l’article est très accessible). L’avantage de ce système c’est que vous n’avez pas à vous préoccuper de quoique ce soit. Le revers de la médaille, c’est que vous ne pouvez pas contrôler quelles requêtes seront mises en cache; tout ce que vous pouvez faire c’est définir le nombre de requêtes maximal pouvant être placé dedans à l’aide des paramètres suivants:

  • maxStatements: le nombre maximal total de requêtes pouvant être acceptées dans le cache.
  • maxStatementsPerConnection: le nombre maximal de requêtes liées à une connexion pouvant être acceptées dans le cache.

Mes sources: ici, ici et .

En voulant me documenter sur le fonctionnement d’InnoDB, j’ai été assez surpris de constater le peu d’information claire et de qualité disponible sur le net à ce sujet.
Je me suis donc procuré une version du « High Performance MySQL » d’O'Reilly et voici un petit résumé de ce que j’ai appris.

Granularité des verrous
Pour conserver un véritable système multi-utilisateur, il faut pouvoir verrouiller uniquement les ressources désirées et non des ensembles trop généraux. Le revers de la médaille est que la performance de l’application en sera nécessairement ralenti: plus la gestion des verrous sera précise et complexe, plus le système y passera du temps. Il est donc primordial de trouver le bon compromis entre concurrence et performance.
Les verrous peuvent ainsi être posés à plusieurs échelles:

  • à l’échelle des tables (possible avec le moteur MyISAM).
  • à l’échelle des pages: le verrou s’applique à une ou plusieurs portions de la table, appelées pages, de tailles identiques et configurables (possible avec le moteur Berkeley DB).
  • à l’échelle des lignes (possible avec InnoDB).

Bien sûr, plus la granularité est fine, plus la gestion de la concurrence est complexe, et plus le risque de voir apparaître des situations d’inter-blocage est grand. Heureusement, InnoDB inclue un système de détection et de résolution des deadlocks en levant des exceptions pour les requêtes concernées.

Les types de verrous
Il existe deux types de verrous:

  • les verrous partagés (shared locks): on autorise les autres threads / processus à accéder aux données en lecture mais pas en écriture. On peut déposer des verrous partagés sur des lignes avec InnoDB à l’aide de la requête SELECT … LOCK IN SHARE MODE.
  • les verrous exclusifs (exclusive locks): on interdit toute lecture ou écriture sur les données marquées par ces verrous. Les verrous exclusifs peuvent être placés avec InnoDB à l’aide de la requête SELECT … FOR UPDATE.

L’utilisation du verrouillage de lignes n’a de sens qu’à l’intérieur d’une transaction. C’est logique: en dehors d’une transaction, le verrouillage sera automatiquement relâché une fois l’exécution de la requête terminée.
Autre remarque importante: une sélection ne prendra pas en compte les verrous déjà posés par une autre transaction si elle n’essaye pas elle même de poser un verrou. Autrement dit une simple requête de type SELECT sans demande de verrouillage partagé ou exclusif ne verra pas les verrous posés par d’autres requêtes dans d’autres transaction. Il faudra donc toujours veiller à verrouiller au minimum à l’aide d’un verrou partagé les données que vous lisez si vous voulez conserver la gestion de la concurrence.

Une dernière remarque, tout aussi importante: si vous exécutez

SELECT * FROM tableX WHERE tableX.champ > 50 FOR UPDATE

ou bien

SELECT * FROM tableX WHERE tableX.champ > 50 LOCK IN SHARE MODE

Si aucun index n’a été défini pour la colonne champ, alors le moteur va devoir lire toutes les lignes de la table pour déterminer lesquelles doivent être sélectionnées. Se faisant il va déposer un verrou sur chacune d’entre elles, et au final c’est toute la table qui sera verrouillée. Pour verrouiller uniquement les lignes désirées, il faudra donc créer un index sur la colonne champ (merci à dbnews pour la précision).

Les transactions
Avec InnoDB, les transactions respectent les 4 règles édictées par le modèle ACID:

    Atomicité: une transaction s’effectue entièrement ou pas du tout.
    Consistance: l’intégrité de la base de données doit être garantie.
    Isolation: les action d’une transaction non validée doivent être invisibles aux autres transactions.
    Durabilité: un crash du système ne doit entamer en rien l’intégrité de la base de données.

Mais le concept d’isolation est plus difficile à mettre en oeuvre qu’il n’y parait. Et de fait il existe quatre degrés d’isolation:

  • Read uncommitted ou dirty read: la règle d’isolation n’est pas respectée (à éviter).
  • Read committed: satisfait la règle de l’isolation mais peut poser problème.
    Exemple:
    Début de la transaction
    Requête 1: sélection de la clé primaire d'une ligne de la table A.
    Requête 2: insertion d'une ligne dans table B référençant la clé précédemment sélectionnée.
    Validation
    

    L’ennui c’est que l’insertion échouera si, entre la première et la seconde requête, une autre transaction supprime la ligne sélectionnée dans A.
    Plus généralement, le mode read committed pose le problème des lectures non répétables, c’est à dire que deux sélections identiques ayant cours dans la même transactions ne retourneront pas forcément le même résultat.

  • Repeatable read: ici le problème des lectures non-répétables est en parti résolu, mais il reste un dernier soucis: les lectures fantômes.
    Exemple:
    Début de la transaction
    Requête 1: select * from A
    // traitement
    Requête 2: select * from A
    Validation
    

    Si une autre requête insère des données dans la table A juste entre l’exécution des requêtes 1 et 2, les deux lectures n’auront pas les mêmes résultats.
    Toutefois avec InnoDB le problème n’est que théorique: il apparaissait tellement souvent que les concepteurs du moteur ont fait en sorte de le régler.

  • Serializable: le plus haut niveau d’isolation en ordonnant les transactions de manière à ce que les lectures fantômes n’apparaissent pas, mais dégrade considérablement les performances.

Avec InnoDB, les transactions ne font intervenir aucun verrou d’aucune sorte. En fait l’isolation est possible grâce à un système de contrôle de concurrence multi-version (Multi-Version Concurrency Control, MVCC). Pour faire simple: un identifiant de création et de suppression est associé à chaque ligne. Ces identifiants jouent le rôle de marqueur temporel, ils permettent donc de travailler sur des instantanés de la base de données et ainsi d’isoler, plus ou moins bien selon le mode utilisé, les transactions les unes des autres.

En fouillant un peu dans les sources du framework Flex, j’ai été intrigué il y a quelques semaines de découvrir des signatures de fonctions assez étranges. J’ai finalement pris le temps de me documenter à leur sujet.

public class Objet {
 
	private var _valeur:int;
 
	public function Objet() {
		_valeur = 0;
	}
 
	public function get valeur():int {
		return _valeur;
	}
 
	public function set valeur(x:int):void {
		_valeur = x;
	}
}

En fait, le mots clé get (respectivement set) agit comme si vous aviez ajouté une variable valeur accessible uniquement en lecture (respectivement écriture), et ces variables s’utilisent ensuite comme n’importe quel autre attribut:

var o:Objet = new Objet();
var x:int = o.valeur;
o.valeur = x + 1;

La ligne suivante est également valide, le compilateur analysera votre code pour savoir quelle fonction (get ou set) appeler selon le contexte:

o.valeur = o.valeur + 1

Vous pouvez placer autant de code que vous voulez à l’intérieur de ces fonctions, elles diffèrent des autres uniquement au moment de l’appel et vous restez donc libre de les implémenter comme vous le souhaitez.
Vous pouvez même écrire des get et set pour un attribut qui n’existe pas en réalité (si sa valeur doit être régénérée à chaque appel ça peut être utile).
En revanche vous ne pouvez pas avoir un attribut du même nom dans votre classe, autrement le compilateur ne saura pas qui fait référence à quoi.

Le nouveau SDK de Flex (version 4) a une politique de compilation par défaut qui pose problème: les dépendances du framework ne sont pas intégrées au code des swf de vos application mais placées dans des fichiers à part. Or, pour des raisons de sécurité, il n’est pas permis d’y accéder quand on lance une application en locale. D’où l’erreur qui suit:
SecurityError: Error #2148: Le fichier SWF file:///C:/Documents and Settings/Proprietaire/Adobe Flash Builder 4/Test/bin-debug/Test.swf ne peut pas accéder à la ressource locale file:///C:/Documents and Settings/Proprietaire/Adobe Flash Builder 4/Test/bin-debug/framework_4.0.0.14159.swf. Seuls les fichiers SWF local-système de fichiers et les fichiers locaux de confiance peuvent accéder aux ressources locales.

Il faut donc veiller lors de la création d’un nouveau projet à bien préciser que les « liaisons de structure » soient fusionnées dans le code. Si votre projet a déjà été crée, la solution consiste à aller dans les propriétés pour demander à ce que les dépendances soient fusionnées: clic droit sur le projet/Propriétés/chemin de génération Flex/Chemin d’accès à la bibliothèque: Liaison de structure -> Fusionné dans le code.

Par contre je n’ai pas trouvé de solution pour fixer cette option par défaut pour tous les projets.

La critique que l’on entend le plus souvent au sujet d’ActionScript concerne l’absence de gestion de la concurrence, c’est à dire l’impossibilité d’effectuer simultanément plusieurs actions (ou du moins avec l’apparence de la simultanéité). En effet, le modèle de gestion des évènements en flash est le suivant: toute action, que ce soit celle d’un utilisateur manipulant un composant graphique ou bien le déclenchement d’un timer, sera systématiquement stockée dans une file d’évènements, et tous les évènements de cette file seront exécuté les uns après les autres.

La plupart du temps cette limitation n’est pas trop dérangeante dans la mesure ou les applications flash/flex nécessitant des calculs lourds ne sont pas courantes; Néanmoins, il peut exister quelque cas où la gestion du multithread devient nécessaire. Dans ces situations là, le problème peut sembler insoluble. Il existe pourtant différentes solutions permettant de contourner de manière plus ou moins efficace cette restriction. Toutes reposent sur la même idée: décomposer un traitement long en une multitude de traitements plus cours; mais toutes ne sont pas équivalentes en terme de performance.

La méthode la plus simple est d’utiliser la fonction callLater qui permet de différer l’exécution d’une fonction: l’évènement que constitue l’exécution prochaine de la fonction est alors placé dans la file d’évènement.
Si l’on part du principe que notre algorithme d’origine ressemble à ça:

public function traitementBoucleTresLong(max:int):void {
	for(var i:int = 0; i < max; i++) {
		// Debut du code a exécuter pour i
		// ...
		// Fin du code a exécuter pour i
	}
}

Alors il nous suffira de le modifier de cette manière:

public function traitementPlusCourt(i:int, max:int):void {
	// Debut du code a exécuter pour i
	// ...
	// Fin du code a exécuter pour i
 
	if(i < max)
		FlexGlobals.topLevelApplication.callLater(traitementPlusCourt, [i + 1, max]);
}

Cette méthode fonctionne mais elle pose de gros problèmes en terme de performance. En fait, si on part du principe qu’une seule itération de notre boucle met 1 milliseconde pour aboutir, alors la deuxième version de notre algorithme mettra environ 40 fois plus de temps que la première à s’exécuter. Comment peut on en être aussi sûr? En fait, c’est là que la raison pour laquelle flash a été crée se rappelle à nous: n’oublions jamais qu’à l’origine flash a été conçu pour réaliser des animations. Or une animation est, par définition, constituée d’une succession d’images statiques se succédant de manière rapide pour donner l’illusion du mouvement. Au cinéma la norme est de 24 images par seconde, et pour flash c’est pareil. 1 seconde équivaut à 1.000 millisecondes. Divisez 1.000 par 24 et vous obtenez environ 41 millisecondes, soit le temps minimal et incompressible accordé par flash à l’exécution d’un évènement. Si une itération met 1 milliseconde à s’exécuter, cela signifie donc que le deuxième algorithme attendra une quarantaine de milliseconde supplémentaires avant de passer la main, et ce à chaque itération. Pas très optimisé n’est ce pas?

Pour éviter de gaspiller autant de ressources inutilement, l’astuce consiste à enchaîner les appels de fonction tant que les 40 millisecondes et quelque ne sont pas écoulées (on veillera juste a arrêter un peu avant la fin du laps de temps imparti). En revanche il devient plus délicat de passer des arguments à la fonction de traitement, on préférera donc créer des variables globales.

private var i:int;
 
private var max:int;
 
public function run():void {
	var dateDebut:Number = getTimer();
	while(i < max && getTimer() - dateDebut < 40 - 1)
		traitementPlusCourt2();
	if(i < max)
		FlexGlobals.topLevelApplication.callLater(run);
}
 
public function traitementPlusCourt2():void {
	// Debut du code a exécuter pour i
	// ...
	// Fin du code a exécuter pour i
}

Le fait de pouvoir bricoler ces threads avec trois fois rien explique qu’on leur ait donné le nom de Green Threads. Pour info, les premières versions de java ne géraient pas le multithread autrement que de cette manière. L’auteur de l’article sur lequel je me suis appuyé pour écrire ce post a crée une librairie permettant d’utiliser facilement cette technique sans devoir réinventer la roue à chaque fois. Il y a même inclus un outils de statistique pour en savoir plus sur le temps total d’exécution de votre algorithme, le nombre d’itérations etc.

j’ai posté en décembre 2009 un article introductif sur les modèles de conception en java, où j’évoquais entre autre le modèle de conception de type observateur.
Aujourd’hui je vais expliquer comment mettre en oeuvre ce modèle avec ActionScript en créant des évènements personnalisés.

Un peu de théorie.
Le modèle événementiel repose, comme en java, sur le modèle de conception observateur. Il distingue:
- un sujet: l’objet écouté (par exemple, un bouton).
- un évènement: l’action de cliquer sur ce bouton.
- un écouteur: la fonction ou la méthode qui sera appelée lors de la diffusion de l’évènement.

Pour pouvoir écouter un évènement associé à sujet donné, il faut que ce dernier hérite de la classe EventDispatcher afin qu’il dispose des méthodes addEventListener et dispatchEvent qui permettent de définir un écouteur et de lancer un évènement.

Si on ne peut affecter plusieurs fois un même écouteur à un même couple (sujet, évènement), on peut en revanche affecter plusieurs écouteurs différents à un même couple.
Voici la signature permettant d’associer à un sujet un évènement et un écouteur:
DispatchEvent.addEventListener(type:String, ecouteur:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void

Concrètement, cela revient à transmettre une référence de l’écouteur au sujet; c’est ainsi que le sujet pourra appeler la fonction désirée lorsque l’évènement associé aura été détecté.
Les deux paramètres les plus importantes transmis lors de l’appel à addEventListener sont:
- Le paramètre type est une chaîne de caractères identifiant le type de l’évènement que l’on cherche à diffuser.
- Le paramètre ecouteur désigne la fonction qui sera exécutée lorsque l’événement sera capté.

Un évènement diffusé est systématiquement accompagné d’un objet héritant de la classe Event. La fonction écouteur aura donc un paramètre du même type, appelé objet évènementiel, contenant entre autres les propriétés suivantes:
- currentTarget: le sujet de l’évènement diffusé.
- type: la chaîne de caractères identifiant le type de l’évènement.

Illustration pratique.
Voilà pour la théorie. Pour la pratique, je vais reprendre l’exemple que j’avais utilisé dans mon précédent article sur le modèle observateur.
Je le rappelle brièvement: on dispose de deux classes Cible et Observateur, et on veut notifier notre observateur lorsqu’une propriété de la cible est modifiée.

Commençons par la classe Cible. Cette classe doit offrir la possibilité de diffuser un évènement, elle doit donc hériter de EventDispatcher:

package {
	import flash.events.EventDispatcher;
	import flash.events.Event;
 
	public class Cible extends EventDispatcher {
 
		private var valeur:int;
 
		public function Cible(valeur:int) {
			super();
			this.valeur = valeur;
		}
 
		public function setValeur(valeur:int):void {
			valeur = this.valeur;
			dispatchEvent(new Event("evenement_perso"));
		}
 
		public function getValeur():int {
			return this.valeur;
		}
	}
}

Dans la mesure ou ActionScript ne gère pas l’héritage multiple, il est parfois nécessaire de trouver une alternative au fait d’étendre EventDispatcher. On préférera alors implémenter l’interface IEventDispatcher. Les appels aux fonctions de IEventDispatcher seront répercutés sur un objet EventDispatcher placé en propriété de Cible:

package {
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
 
	public class Cible implements IEventDispatcher {
 
		private var valeur:int;
 
		private var dispatcher:EventDispatcher;
 
		public function Cible(valeur:int) {
			this.valeur = valeur;
			this.dispatcher = new EventDispatcher();
		}
 
		public function setValeur(valeur:int):void {
			this.valeur = valeur;
		}
 
		public function getValeur():int {
			return this.valeur;
		}
 
		public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void {
			this.dispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
		}
 
		public function removeEventListener(type:String, listener:Function, useCapture:Boolean=false):void {
			this.dispatcher.removeEventListener(type, listener, useCapture);
		}
 
		public function dispatchEvent(event:Event):Boolean {
			return this.dispatcher.dispatchEvent(event);
		}
 
		public function hasEventListener(type:String):Boolean {
			return this.dispatcher.hasEventListener(type);
		}
 
		public function willTrigger(type:String):Boolean {
			return this.dispatcher.willTrigger(type);
		}
	}
}

Passons à présent au code de notre classe Observateur:

package {
	import flash.events.Event;
	import Cible;
 
	public class Observateur {
 
		private var cible1:Cible;
 
		private var cible2:Cible;
 
		public function Observateur() {
			cible1 = new Cible(5);
			cible2 = new Cible(10);
			cible1.addEventListener("evenement_perso", update);
			cible2.addEventListener("evenement_perso", update);
 
			cible1.setValeur(20);
		}
 
		public function update(event:Event):void {
			var cible:Cible = event.target as Cible;
			var valeur:int = cible.getValeur();
			trace("Nouvelle valeur récupérée");
		}
	}
}

Et voilà, la mise en oeuvre du modèle de conception observateur en ActionScript est aussi simple que cela. On pourra si nécessaire étendre la classe Event si l’on souhaite transmettre à l’écouteur des informations qui ne sont pas directement accessible à l’aide de la cible.

Maintenant, on pourrait se demander pourquoi passer par une structure aussi « sophistiquée » alors qu’on pourrait aboutir au même résultat en transférant simplement à la cible une copie de l’instance Observateur. De cette manière, la cible pourrait appeler directement la fonction update de l’observateur. En fait, l’utilisation des événements nous permet de conserver une certaine modularité dans notre application, et facilite ainsi la maintenance du code.