|
|
Les domaines de synchronisation pour la gestion des accès concurrents par Patrick Smacchia |
L’attribut System.Runtime.Remoting.Contexts.Synchronization
La notion de domaine de
synchronisation
L’attribut
System.Runtime.Remoting.Contexts.Synchronization et les domaines de
synchronisation
La gestion de la réentrance dans un
domaine de synchronisation
Pour une bonne compréhension de cet article, il est nécessaire d'avoir une connaissance des notions suivantes:
· domaine d’application,
· contexte .NET,
· classe context-bound/context-agile,
· intercepteur de messages.
Toutes ces concepts sont notamment expliquées dans mon ouvrage Pratique de .NET et C# (O’Reilly 2003).
Lorsque l’attribut System.Runtime.Remoting.Contexts.Synchronization est appliqué à une classe, une instance de cette classe ne peut être accédée par plus d’un thread à la fois. Cependant, pour que ce comportement puisse s’appliquer, il faut que la classe cible soit context-bound. Précisons qu’une classe est context-bound si elle dérive directement ou indirectement de la classe System.ContextBoundObject. Voici un exemple où l’utilisation de cet attribut sur la classe UneClasse permet de synchroniser les accès concurrents de deux threads sur une instance de cette classe:
using System;
using System.Runtime.Remoting.Contexts;
using System.Threading;
[Synchronization(SynchronizationAttribute.REQUIRED)]
public class UneClasse : ContextBoundObject
{
public void AfficheThreadHashVal()
{
Console.WriteLine("Début:
ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
Thread.Sleep(1000);
Console.WriteLine("Fin: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
}
}
public class Prog
{
static UneClasse m_Objet;
static void
{
m_Objet
= new UneClasse1();
Thread
T1 = new Thread(new
ThreadStart(ThreadProc));
Thread
T2 = new Thread(new
ThreadStart(ThreadProc));
T1.Start(); T2.Start();
T1.Join();
T2.Join();
}
static void ThreadProc()
{
for(int i=0;i<2;i++)
m_Objet.AfficheThreadHashVal();
}
}
Cet exemple affiche :
Début: ThreadHashVal= 27
Fin: ThreadHashVal= 27
Début: ThreadHashVal= 28
Fin: ThreadHashVal= 28
Début: ThreadHashVal= 27
Fin: ThreadHashVal= 27
Début: ThreadHashVal= 28
Fin: ThreadHashVal= 28
On parle dés fois de synchronisation automatique pour désigner un tel mécanisme. En effet, il incombe au CLR et non pas au développeur, d’empêcher les accès concurrents. Cette magie est mise en œuvre grâce au mécanisme d’interception des appels du CLR (notamment utilisé par .NET Remoting). C’est pour cette raison que la classe sur laquelle s’applique cet attribut doit être context-bound, car le mécanisme d’interception des appels du CLR peut s’appliquer sur les instances d’une classe à la condition que celle-ci soit context-bound. Cependant, il faut savoir que l’utilisation de ce mécanisme peu avoir un coût non négligeable si de très nombreux appels de méthodes sont interceptés.
Allons un peu plus loin dans la compréhension de la synchronisation automatique. Notamment, que signifie ce paramètre REQUIRED passé à l’attribut System.Runtime.Remoting.Contexts.Synchronization?
Un domaine de synchronisation est une entité entièrement prise en charge par le CLR. Un tel domaine contient un ou plusieurs contextes .NET et donc, contient les objets de ces contextes. Un contexte .NET ne peut appartenir qu’à au plus un seul domaine de synchronisation. En outre, la notion de domaine de synchronisation est plus fine que la notion de domaine d’application. La figure suivante illustre les relations d’inclusions entre processus, domaines d’application, domaines de synchronisation, contextes .NET et objets .NET :

Puisque nous parlons de la synchronisation, et donc des applications multi-threaded, il est utile de rappeler que les domaines d’applications et les threads d’un processus sont deux notions orthogonales. En effet, un thread peut traverser librement la frontière entre deux domaines d’application, par exemple en appelant un objet A situé dans le domaine d’application DA, à partir d’un objet B situé dans le domaine d’application DB. De plus, le code d’un domaine d’application peut être exécuté simultanément par zéro, un ou plusieurs threads.
En revanche, tout l’intérêt d’un domaine de synchronisation réside dans le fait qu’il ne peut être partagé simultanément par plusieurs threads. Autrement dit, les méthodes des objets contenus dans un domaine de synchronisation ne peuvent être simultanément exécutées par plusieurs threads. Ceci implique qu’à un instant donné, au plus un thread se trouve dans domaine de synchronisation. On parle alors de droits d’accès exclusifs au domaine de synchronisation. Encore une fois, la gestion de ces droits d’accès exclusifs est entièrement assurée par le CLR.
Vous avez peut-être déjà deviné que l’attribut System.Runtime.Remoting.Contexts.Synchronization sert en fait à indiquer au CLR quand créer un domaine de synchronisation et comment en délimiter sa frontière. Ces réglages se font en utilisant dans la déclaration d’un attribut System.Runtime.Remoting.Contexts.Synchronization une des quatre valeurs suivantes NOTSUPPORTED, SUPPORTED, REQUIRED ou REQUIRES_NEW. Notez que la valeur REQUIRED est choisie par défaut.
L’appartenance à un domaine d’application se communique de proche en proche lorsqu’un objet en crée un autre. On parle ainsi d’objet créateur, mais prenez en compte qu’un objet peut être aussi créé au sein d’une méthode statique. Or, il se peut que l’exécution de la méthode statique soit le fruit d’un appel d’un objet situé dans un domaine de synchronisation. Il faut alors savoir que dans ce cas, la méthode statique propage l’appartenance au domaine d’application et joue ainsi le rôle d’un objet créateur.
Voici l’explication des quatre comportements possibles :
|
NOT_SUPPORTED |
Ce paramètre assure qu’une
instance de la classe sur laquelle s’applique l’attribut Synchronization n’appartiendra jamais à un domaine de synchronisation (que son
objet créateur appartienne à un domaine de synchronisation ou non). |
|
SUPPORTED |
Ce paramètre indique
qu’une instance de la classe sur laquelle s’applique l’attribut Synchronization n’a pas besoin d’appartenir à un domaine de synchronisation.
Néanmoins, une telle instance appartiendra au domaine de synchronisation de
son objet créateur si ce dernier en a un. Ce comportement est peu utile, car
le développeur doit quand même prévoir un autre mécanisme de synchronisation
au sein des méthodes de la classe. Cependant, il peut éventuellement
permettre de propager l’appartenance à un domaine de synchronisation à partir
d’un objet qui n’a pas besoin d’être synchroniser (ce type d’objet est rare,
puisqu’il ne doit pas avoir d’état). |
|
REQUIRED |
Ce paramètre assure
qu’une instance de la classe sur laquelle s’applique l’attribut Synchronization sera de toutes façons dans un domaine de synchronisation. Si
l’objet créateur appartient déjà à un domaine de synchronisation, alors on se
satisfera de celui là. Sinon, un nouveau domaine de synchronisation est créé
pour accueillir ce nouvel objet. |
|
REQUIRES_NEW |
Ce paramètre assure qu’une
instance de la classe sur laquelle s’applique l’attribut Synchronization sera dans un nouveau domaine de synchronisation (que son objet
créateur appartienne à un domaine de synchronisation ou non). |
Ces comportements peuvent se résumer ainsi:
|
La valeur appliquée: |
Est-ce que l’objet
créateur est dans un domaine de synchronisation? |
L’objet créé
résidera… |
|
NOT_SUPPORTED |
Non |
…à l’extérieur de tout
domaine de synchronisation. |
|
Oui |
||
|
SUPPORTED |
Non |
…à l’extérieur de tout
domaine de synchronisation. |
|
Oui |
…dans le domaine de
synchronisation de l’objet créateur. |
|
|
REQUIRED |
Non |
…dans un nouveau domaine
de synchronisation. |
|
Oui |
…dans le domaine de
synchronisation de l’objet créateur. |
|
|
REQUIRES_NEW |
Non |
…dans un nouveau domaine
de synchronisation. |
|
Oui |
Dans un domaine de synchronisation D, lorsque le thread T1 qui a les droits d’accès exclusifs effectue un appel sur un objet situé hors de D, deux comportements peuvent alors être appliqués par le CLR:
Notez que nous avons vu qu’un appel à une méthode statique n’est pas considéré comme un appel à l’extérieur d’un domaine de synchronisation.

Certains constructeurs de la classe System.Runtime.Remoting.Contexts.Synchronization acceptent un booléen en paramètre. Ce booléen définit s’il y a réentrance lors d’un appel hors du domaine de synchronisation courant. Lorsque ce booléen est positionné à true, il y a réentrance. Notez qu’il suffit que l’attribut de synchronisation de la classe du premier objet rencontré par un thread dans un domaine de synchronisation ait positionné la réentrance à true pour qu’il y ait effectivement réentrance.
L’exemple suivant est l’illustration par le code de la figure ci-dessus :
using System;
using System.Runtime.Remoting.Contexts;
using System.Threading;
[Synchronization(SynchronizationAttribute.REQUIRES_NEW,true)]
public class
UneClasse1 : ContextBoundObject
{
public void
AfficheThreadHashVal()
{
Console.WriteLine("UneClasse1
Début: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
Thread.Sleep(1000);
UneClasse2
obj2 = new
UneClasse2();
obj2.AfficheThreadHashVal();
Console.WriteLine("UneClasse1
Fin: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
}
}
[Synchronization(SynchronizationAttribute.REQUIRED)]
public class
UneClasse2 : ContextBoundObject
{
public void
AfficheThreadHashVal()
{
Console.WriteLine("UneClasse2
Début: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
Thread.Sleep(1000);
UneClasse3
obj3 = new
UneClasse3();
obj3.AfficheThreadHashVal();
Console.WriteLine("UneClasse2
Fin: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
}
}
// on est sur que les instances de cette
classe seront hors de tous domaine de synchronisation
[Synchronization(SynchronizationAttribute.NOT_SUPPORTED)]
public class
UneClasse3 : ContextBoundObject
{
public void
AfficheThreadHashVal()
{
Console.WriteLine("UneClasse3
Début: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
Thread.Sleep(1000);
Console.WriteLine("UneClasse3
Fin: ThreadHashVal= " +
Thread.CurrentThread.GetHashCode()
);
}
}
public class
Prog
{
static UneClasse1 m_Objet;
static void
{
m_Objet
= new UneClasse1();
Thread
T1 = new Thread(new
ThreadStart(ThreadProc));
Thread
T2 = new Thread(new
ThreadStart(ThreadProc));
T1.Start(); T2.Start();
T1.Join();
T2.Join();
}
static void
ThreadProc()
{
m_Objet.AfficheThreadHashVal();
}
}
Cet exemple affiche ceci:
UneClasse1 Début: ThreadHashVal= 3
UneClasse2 Début: ThreadHashVal= 3
UneClasse1 Début: ThreadHashVal= 4
UneClasse2 Début: ThreadHashVal= 4
UneClasse3 Début: ThreadHashVal= 4
UneClasse3 Début: ThreadHashVal= 3
UneClasse3 Fin:
ThreadHashVal= 4
UneClasse2 Fin:
ThreadHashVal= 4
UneClasse1 Fin:
ThreadHashVal= 4
UneClasse3 Fin:
ThreadHashVal= 3
UneClasse2 Fin:
ThreadHashVal= 3
UneClasse1 Fin:
ThreadHashVal= 3
Il est clair que si l’on avait désactivé la réentrance dans UneClasse1, l’affichage de ce programme aurait été le suivant :
UneClasse1 Début: ThreadHashVal= 3
UneClasse2 Début: ThreadHashVal= 3
UneClasse3 Début: ThreadHashVal= 3
UneClasse3 Fin: ThreadHashVal= 3
UneClasse2 Fin: ThreadHashVal= 3
UneClasse1 Fin:
ThreadHashVal= 3
UneClasse1 Début: ThreadHashVal= 4
UneClasse2 Début: ThreadHashVal= 4
UneClasse3 Début: ThreadHashVal= 4
UneClasse3 Fin:
ThreadHashVal= 4
UneClasse2 Fin:
ThreadHashVal= 4
UneClasse1 Fin:
ThreadHashVal= 4
La réentrance est utilisée pour optimiser la gestion des ressources car elle permet de réduire globalement les durées d’accès exclusifs des threads sur les domaines de synchronisation. Cependant par défaut, la réentrance est désactivée. En effet, lorsqu’il y a réentrance, notre compréhension de la notion de domaine de synchronisation est fortement perturbée. Bien pire encore, activer à tort et à travers la réentrance peut amener à des situations de deadlock. Pour ces raisons, il est fortement déconseillé d’activer la réentrance.
Les domaines de synchronisation constituent un mécanisme très puissant pour gérer le problème des accès concurrents dans vos applications multi-threaded. Le désavantage majeur est le coût entraîné par les interceptions des appels. Son intérêt vient à la fois de sa capacité à automatiser les obtentions et les libérations de droits d’accès exclusifs, et du fait qu’il oblige le développeur à raisonner en termes d’objets (et de groupes d’objets) synchronisés et non en termes d’objets de synchronisation. Bien évidemment, cette possibilité du Framework .NET est à rapprocher de la notion d’appartements COM et d’activités COM+. Cependant, la simplicité d’utilisation des domaines de synchronisation par rapport à ces mécanismes témoigne de l’effort global de Microsoft pour faciliter la tache des développeurs.
Auteurs : Patrick Smacchia
Copyright © Septembre 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. |