|
|
Bonnes pratiques de programmation avec C# (seconde édition) par Patrick Smacchia |
|
|
|
|
|
Épisode 2
Impression papier : environ 9 pages
Version 1.0
Un
gros avantage du mot-clé C# lock sur la classe Monitor
Partager
un événement entre processus
Exécuter
les méthodes abonnées à un événement d’une manière asynchrone
Se
protéger des exceptions lancées par les méthodes abonnées à un événement
Lors de la discussion autour d’un précédent article concernant FxCop, je pensais qu’il serait intéressant que cet outil signale des dépassements de seuils de métrique de code. De là m’est venu l’idée de mesurer quelques valeurs pertinentes concernant le Framework .NET 1.1. Voici les résultats.
|
Description de la métrique |
Moyenne |
Ecart Type |
Echantillon |
Maximum |
|
Nombre
de méthodes dans les interfaces publiques (Les
surcharges d’une méthode comptent toutes pour 1) |
2.88 |
3.63 |
271
interfaces |
23
méthodes pour l’interface System._AppDomain |
|
Nombre
de méthodes dans les interfaces publiques (Chaque
surcharge d’une méthode compte pour 1) |
3.15 |
4.52 |
271
interfaces |
43
méthodes pour l’interface System._AppDomain |
|
Nombre
de méthodes dans les classes publiques (Les
surcharges d’une méthode comptent toutes pour 1) (*) |
23.72 |
44.04 |
2291
classes |
446
méthodes pour la classe System.Windows.Forms.DataGrid |
|
Nombre
de méthodes dans les classes publiques (Chaque
surcharge d’une méthode compte pour 1) (*) |
26,48 |
48.19 |
2291
classes |
473
méthodes pour la classe System.Windows.Forms.DataGrid |
|
Nombre
de méthodes publiques dans les classes publiques (Les
surcharges d’une méthode comptent toutes pour 1) (*) |
10.26 |
9.22 |
2291
classes |
86
méthodes pour la classe System.Runtime.InteropServices.Marshal |
|
Nombre
de méthodes publiques dans les classes publiques (Chaque
surcharge d’une méthode compte pour 1) (*) |
12.44 |
15.19 |
2291
classes |
331
méthodes pour la classe System.Convert |
|
Nombre
de propriétés dans les interfaces publiques |
1.00 |
1.94 |
271
interfaces |
141
propriétés pour l’interface Accessibility.IAccessible |
|
Nombre
de propriétés dans les classes publiques |
7.12 |
14.03 |
2291
classes |
14 propriétés
pour les classes System.Drawing.Brushes et System.Drawing.Pens |
|
Nombre
de paramètres des méthodes des interfaces publiques |
1.62 |
1.67 |
855
méthodes |
13
paramètres pour la méthode AddServerTlb() de l’interface System.EnterpriseServices.Internal.ISoapServerTlb |
|
Nombre
de paramètres des méthodes des classes publiques |
0.98 |
1.19 |
60665 méthodes |
15
paramètres pour la méthode PushScanner() de la classes System.Xml.XmlTextReader |
|
Nombre
de paramètres des méthodes publiques des classes publiques |
0.90 |
1,15 |
28508
méthodes |
14
paramètres pour la méthode DrawBorder() de la classes System.Windows.Forms.ControlPaint |
|
Profondeur
des classes dans l’arbre d’héritage d’implémentation (**) |
2.00 |
1.24 |
5467
classes |
Profondeur
de 10 pour la classe System.Xml.Schema.Datatype_unsignedByte |
|
Profondeur
des classes publiques dans l’arbre d’héritage
d’implémentation (**) |
2.24 |
1.14 |
2291
classes |
Profondeur
de 7 pour les classes System.Web.UI.Design.WebControls.CalendarAutoFormatDialog System.Web.UI.Design.WebControls.RegexEditorDialog System.Messaging.Design.QueuePathDialog System.ServiceProcess.Design.ServiceInstallerDialog System.Windows.Forms.DomainUpDown System.Windows.Forms.NumericUpDown System.Windows.Forms.ThreadExceptionDialog System.Windows.Forms.Design.ComponentEditorForm System.Windows.Forms.PrintPreviewDialog |
|
Nombre
d’interfaces implémentées ou héritées par une classe |
1.14 |
2.69 |
5467
classes |
19
interfaces implémentées ou héritées par les classes System.Windows.Forms.Design.ComponentTray System.Windows.Forms.Design.ComponentDocumentDesigner System.Windows.Forms.PropertyGrid |
|
Nombre
d’interfaces implémentées ou héritées par une classe publique |
1.50 |
2.76 |
2291
classes |
19
interfaces implémentées ou héritées par les classes System.Windows.Forms.Design.ComponentTray System.Windows.Forms.PropertyGrid |
|
Nombre
de classes par espace de noms |
43.06 |
61.62 |
126
espaces de noms |
564
classes contenues dans l’espace de noms System.Windows.Forms |
|
Nombre
de classes publiques par espace de noms |
18.17 |
27.07 |
126
espaces de noms |
206
classes publiques contenues dans l’espace de noms System.Windows.Forms |
|
Nombre
de types valeur par espace de noms |
9.02 |
16.93 |
126
espaces de noms |
145
types valeur contenus dans l’espace de noms System.Windows.Forms |
|
Nombre
de types valeur publics par espace de noms |
4.60 |
10.92 |
126
espaces de noms |
96
types valeur publics contenus dans l’espace de noms System.Windows.Forms |
(*) Les six méthodes suivantes de la classe System.Object (implémentées par toutes les classes du Framework) sont prises en compte dans le calcul de ces statistiques: Finalize() GetHashCode() Equals() ToString() GetType() MemberwiseClone()
(**) Le fait que toutes les classes dérivent de la classe System.Object et ont donc au moins une profondeur de 1 dans l’arbre d’héritage d’implémentation, est pris en compte dans le calcul de ces statistiques.
Vous pouvez télécharger le petit
programme qui m’a servi à obtenir ces valeurs
ici. Ce programme peut
aussi vous servir à obtenir ces valeurs sur vos propres assemblages. Il suffit
de remplacer la fonction LoadSystemAssemblies() par une fonction qui
charge vos assemblage dans le domaine d’application courant. Ne tenez pas compte de l’assemblage nommé mscorlib.
Nous avions vu dans le précédent article concernant les bonnes pratiques de programmation avec C#, que l’utilisation du mot clé lock de C# provoquait l’utilisation de la classe de synchronisation System.Threading.Monitor. La signature des méthodes statiques Enter() et Exit() de cette classe est la suivante :
public static void
Enter(object o)
public static void Exit(object o)
Ainsi, lorsque vous souhaitez synchroniser une portion de code avec la classe Monitor, il vous faut fournir un objet par référence. Un problème se pose cependant lorsque vous fournissez par référence un objet de type valeur (i.e une structure ou une instance d’un type primitif, int, double… excepté string). En effet, un tel objet est alors boxé, c’est à dire qu’une copie de cet objet est automatiquement réalisée dans le tas du process courant, et une référence vers cette copie est passée. Le problème est qu’une telle copie est réalisée à chaque appel de la méthode Enter() et à chaque appel de la méthode Exit(). En conséquence, tout se passe comme si l’on synchronisait notre portion de code avec un objet différent à chaque passage. Il n’y a donc aucune synchronisation réalisée ! Pour vous en convaincre, analysez le programme suivant qui s’exécute en 5 secondes lorsque SyncObj est de type int (type valeur) et en 10 secondes lorsque SyncObj est de type string (type référence).
using System;
using System.Threading;
class Prog
{
static void
{
Thread t1
= new Thread(new ThreadStart( f ));
Thread t2
= new Thread(new ThreadStart( f ));
t1.Start();t2.Start();t1.Join();
t2.Join();
}
static int SyncObj = 0;
//static string SyncObj = "Foo";
static void f()
{
for(int i =0;i<5;i++)
{
try
{
Monitor.Enter(SyncObj);
Console.WriteLine("{0}",DateTime.Now.Second);
Thread.Sleep(1000);
}
finally
{
Monitor.Exit(SyncObj);
}
}
}
}
Ce problème est d’autant plus vicieux que le compilateur n’a aucun moyen de vous prévenir puisqu’il est autorisé à générer une opération de boxing lorsqu’une instance d’un type valeur doit être utilisée par l’intermédiaire d’une référence. Une bonne nouvelle est que l’utilisation du mot-clé lock modifie ce comportement du compilateur C#, et ce dernier produit une erreur si l’objet avec lequel on se synchronise est de type valeur. Voici donc une raison autre que le bienfait syntaxique d’utiliser le mot clé lock à la place de la classe Monitor.
Remarquez que la fonctionnalité apportée par les méthodes
statiques Wait(), Pulse() et PulseAll() de la classe Monitor, n’est
pas couverte par l’utilisation du mot-clé lock. Ainsi, lors
de l’utilisation de cette fonctionnalité la classe Monitor est utilisée
explicitement. On pourrait arguer que dans ce cas il est toujours possible de
se synchroniser par inadvertance sur une instance d’un type valeur sans
être averti par le compilateur. Cependant, une telle faute entraîne des durées d’attentes
anormales (voire infinies), et produit donc un comportement aberrant facilement
détectable.
Nous allons montrer ici une façon de contourner une petite incohérence du Framework .NET. En effet, à l’instar de l’API win32, le Framework .NET nous permet de nommer les mutex (classe System.Threading.Mutex). Rappelons que l’intérêt principal d’un mutex nommé est qu’il peut être utilisé pour synchroniser des threads qui ne sont pas dans le même processus. En outre l’API win32 permet également de nommer les événement et donc, de les partager entre processus. Bizarrement, les événements du Framework .NET (classe System.Threading.ManualResetEvent et System.Threading.AutoResetEvent) ne peuvent pas être nommés, et par conséquent un événement .NET ne peut pas être utilisé par plusieurs processus. Ce problème est assez gênant puisque de nombreuses architectures multi-processus requièrent de partager un événement.
La solution proposée par la classe suivante est très simple : elle encapsule (wrappe) les fonctions de l’API win32 relatives aux évènements nommés. Elle constitue en outre un bon exemple de l’utilisation du mécanisme Platform Invoke qui permet d’appeler des fonctions de DLLs natives à partir de vos programmes .NET:
using System;
using System.Runtime.InteropServices ;
public class NamedEvent
{
[DllImport("kernel32",
EntryPoint="CreateEvent",
SetLastError = true)]
private static extern IntPtr CreateEvent(
IntPtr eventAttributes,
bool bManualReset,
bool bInitialState,
String Name);
[DllImport("kernel32",
EntryPoint="WaitForSingleObject",
SetLastError = true)]
private static extern int WaitForSingleObject(IntPtr hEvent, int dwMilliseconds);
[DllImport("kernel32",
EntryPoint="SetEvent",
SetLastError = true)]
private static extern bool SetEvent(IntPtr hEvent);
[DllImport("kernel32",
EntryPoint="ResetEvent",
SetLastError = true)]
private static extern bool ResetEvent(IntPtr hEvent);
[DllImport("kernel32",
EntryPoint="CloseHandle",
SetLastError = true)]
private static extern IntPtr CloseHandle(IntPtr hEvent);
private const int
WAIT_0 = 0x00000000;
private const int
WAIT_TIMEOUT = 0x00000102;
private const int
WAIT_ABANDONED = 0x00000080;
private IntPtr
m_Handle = IntPtr.Zero;
public enum WaitStatus{WAIT_0,WAIT_TIMEOUT,WAIT_ABANDONED,WAIT_UNKNOWN}
public NamedEvent(bool bManualReset,bool bInitialState,String
Name)
{
m_Handle = CreateEvent(IntPtr.Zero,bManualReset,bInitialState,Name);
}
public WaitStatus
Wait(int dwMilliseconds)
{
int
Ret = WaitForSingleObject(m_Handle,dwMilliseconds);
switch(Ret)
{
case WAIT_0:
return WaitStatus.WAIT_0;
case WAIT_TIMEOUT:
return WaitStatus.WAIT_TIMEOUT;
case WAIT_ABANDONED:
return WaitStatus.WAIT_ABANDONED;
}
return
WaitStatus.WAIT_UNKNOWN;
}
public bool SetEvent()
{
return SetEvent(m_Handle);
}
public bool ResetEvent()
{
return ResetEvent(m_Handle);
}
public void CloseHandle()
{
if( m_Handle
!= IntPtr.Zero )
CloseHandle(m_Handle);
m_Handle = IntPtr.Zero;
}
}
Attention, ici nous parlons d’événements .NET (mot-clé C# event) et non pas d’événements de synchronisation comme dans la section précédente.
Lorsqu’un évènement est déclenché, il faut bien être conscient que c’est le même thread qui exécute l’instruction de déclenchement et les méthodes abonnées à l’évènement. Un problème potentiel est que certaines méthodes abonnées s’exécutent en un temps inacceptable. Un autre problème potentiel est qu’une méthode abonnée peut ne pas rattraper une exception. Cette dernière empêchera alors les exécutions des autres méthodes abonnées. De plus une exception lancée dans une méthode abonnée ne devrait pas être propagée dans la méthode qui déclenche un événement. En effet, cette dernière doit être totalement découplée des méthodes abonnées.
Pour pallier ces problèmes, il serait bien pratique que les méthodes abonnées soient exécutées d’une manière asynchrone. En outre, il serait aussi intéressant que différents threads exécutent les différentes méthodes abonnées. Ainsi, une méthode abonnée qui s’exécute en un temps inacceptable ou qui lance une exception ne gène ni l’exécution du déclencheur de l’événement, ni les exécutions des autres méthodes abonnées. En fait, le pool de threads .NET est particulièrement adapté à ces deux contraintes.
Ceux qui connaissent la syntaxe d’un appel asynchrone de méthode risquent d’être tentés d’utiliser directement la fonction BeginInvoke() sur l’événement lui-même. Le problème de cette solution est que seule une méthode abonnée sera exécutée. La bonne solution, illustrée par l’exemple suivant (tiré de l’exemple 13-9 de Pratique de .NET et C#) nécessite le parcours explicite de la liste des méthodes abonnées: (Notez qu’il n’y a pas lieu d’appeler la méthode EndInvoke() puisque les méthodes abonnées ne sont pas sensées retourner de résultat à la méthode qui a déclenché l’événement).
using System;
class Abonné
{
private string
m_Nom;
public
Abonné( string Nom ) { m_Nom
= Nom; }
// Méthode à
appeler lorsqu'un événement est déclenché
// (i.e lorsqu'un bulletin d'information est publié).
public void ReceptionInfo(object
sender, EventArgs e)
{
InfoEventArgs Info = e as InfoEventArgs;
if( Info != null )
{
Console.WriteLine(m_Nom + " reçoit
l'info:" + ((InfoEventArgs)e).GetBulletinInfo() );
}
}
}
//La classe d’argument de l’événement bulletin
d'information
class InfoEventArgs: EventArgs
{
private string m_Description;
public string GetBulletinInfo() { return m_Description; }
public InfoEventArgs
(string Description){ m_Description
= Description; }
}
// Définition du type délégué 'handler
d'un bulletin d'information'
public delegate void
InfoEventHandler(object
sender, EventArgs e);
// Classe contenant les événements bulletin d'information
class AgenceDePresse
{
// Définition des
événements bulletins d'information.
public
event InfoEventHandler InfoFrance;
public
event InfoEventHandler InfoMonde;
// Méthodes de
déclenchement des événements
// (i.e de publication de bulletin d'information).
public
void OnInfoFrance(InfoEventArgs BulletinInfo)
{
if( InfoFrance != null )
{
// Ce code
permet l’exécution asynchrone des méthodes abonnées à l’évènement
Delegate[] DTab
= InfoFrance.GetInvocationList();
foreach(Delegate
tmp in DTab )
{
InfoEventHandler
Dtmp = (InfoEventHandler) tmp;
Dtmp.BeginInvoke(this,BulletinInfo,null,null);
}
// au lieu de :
// if( InfoFrance != null ) InfoFrance(this,BulletinInfo)
// attend un peu pour permettre les exécutions asynchrones
// celles ci se faisant sur des threads
du pool,
// qui sont de threads background
System.Threading.Thread.Sleep(100);
}
}
public void OnInfoMonde(InfoEventArgs BulletinInfo)
{
if( InfoMonde
!= null )
InfoMonde(this,BulletinInfo);
}
}
class Prog
{
public static void
{
// Création de
l'agence de presse.
AgenceDePresse AgencePresse = new AgenceDePresse();
// Création des
abonnés.
Abonné Raymond = new
Abonné("Raymond");
Abonné
Olivier = new Abonné("Olivier");
Abonné Mathieu = new
Abonné("Mathieu");
// Création des
abonnements aux événements bulletins d'information.
AgencePresse.InfoFrance += new
InfoEventHandler(Raymond.ReceptionInfo );
AgencePresse.InfoFrance += new
InfoEventHandler(Olivier.ReceptionInfo );
AgencePresse.InfoMonde += new InfoEventHandler(Olivier.ReceptionInfo );
AgencePresse.InfoMonde += new InfoEventHandler(Mathieu.ReceptionInfo );
// Publication de
bulletins d'information
//(déclenchement
des événement).
AgencePresse.OnInfoFrance(new InfoEventArgs("Hausse
du prix du tabac."));
AgencePresse.OnInfoMonde(new InfoEventArgs("Nouvelle
élection au Etats-Unis."));
// Résilation d'abonnement.
AgencePresse.InfoFrance -= new
InfoEventHandler(Olivier.ReceptionInfo );
// Publication de
bulletins d'information.
AgencePresse.OnInfoFrance(new InfoEventArgs
("Baisse des impôts."));
}
}
Invoquer les méthodes abonnées à un événement d’une manière asynchrone est une technique efficace pour protéger le code qui déclenche un évènement des exceptions remontées par les méthodes abonnées. Il est cependant possible d’avoir le même niveau de protection en invoquant les méthodes d’une manière synchrone. Pour cela, il suffit d'invoquer les différentes méthodes abonnées une à une, comme le montre l’exemple suivant:
...
// Méthodes de déclenchement
des événements
// (i.e de publication de bulletin d'information).
public
void OnInfoFrance(InfoEventArgs BulletinInfo)
{
if( InfoFrance != null )
{
Delegate[] DTab = InfoFrance.GetInvocationList();
foreach(Delegate tmp in DTab )
{
InfoEventHandler
Dtmp = (InfoEventHandler) tmp;
try
{
Dtmp(this,BulletinInfo);
}
catch(Exception){/*traitement
d'exception*/}
}
}
}
...
Auteurs : Patrick
Smacchia
Copyright © Décembre 2003
|
Patrick Smacchia, email [patrick@smacchia.com] Patrick Smacchia assure de nombreuses formations sur .NET, à la fois dans l’industrie et dans le milieu universitaire (Université de Nice). Passionné par l’architecture logicielle, il aide les entreprises à concevoir et à développer leurs applications. Ingénieur diplômé de l’ENSEEIHT, il a notamment collaboré avec Amadeus et avec les divisions espace et téléphonie mobile d’Alcatel. Son site expose plus en détail ses activités. Ses compétences ont été reconnues par Microsoft France, ce qui lui a valu la distinction MVP .NET (Most Valuable Professional sur les technologies .NET). L’ouvrage Pratique de .NET et C# (O’Reilly 2003) L’ ouvrage Pratique de .NET et C# (O’Reilly 2003) couvre la plupart des aspects du développement sous .NET avec le langage C# (architecture .NET sous jacente; langage C#, bibliothèques ADO.NET, XML, WinForm, GDI+, architectures distribuées avec COM+, .NET Remoting et ASP.NET etc.). Cet ouvrage contient de nombreux rappels pour le rendre accessible aux étudiants et aux débutants. Les développeurs confirmés pourront quant à eux rapidement exploiter les subtiles possibilités proposées par .NET, que sont par exemple la réflexion, la programmation orientée aspect ou le mécanisme d’attribut. |