|
|
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.