Quelle est l’utilisation du modèle SyncRoot?

Je suis en train de lire le cahier ac qui décrit le modèle SyncRoot. Ça montre

void doThis() { lock(this){ ... } } void doThat() { lock(this){ ... } } 

et compare au modèle SyncRoot:

 object syncRoot = new object(); void doThis() { lock(syncRoot ){ ... } } void doThat() { lock(syncRoot){ ... } } 

Cependant, je ne comprends pas vraiment la différence ici; il semble que dans les deux cas, les deux méthodes ne sont accessibles que par un seul thread à la fois.

Le livre décrit … parce que l’object de l’instance peut également être utilisé pour un access synchronisé de l’extérieur et que vous ne pouvez pas contrôler cette forme, vous pouvez utiliser le modèle SyncRoot Eh? ‘object de l’instance’?

Quelqu’un peut-il me dire la différence entre les deux approches ci-dessus?

Merci d’avance

    Si vous avez une structure de données interne que vous souhaitez empêcher l’access simultané par plusieurs threads, vous devez toujours vous assurer que l’object sur lequel vous verrouillez n’est pas public.

    Le raisonnement derrière ceci est qu’un object public peut être verrouillé par n’importe qui, et que vous pouvez donc créer des blocages, car vous ne contrôlez pas totalement le schéma de locking.

    Cela signifie que verrouiller sur this n’est pas une option, car tout le monde peut verrouiller cet object. De même, vous ne devez pas verrouiller quelque chose que vous exposez au monde extérieur.

    Ce qui signifie que la meilleure solution consiste à utiliser un object interne et que, par conséquent, il suffit d’utiliser Object .

    Verrouiller les structures de données est quelque chose dont vous avez vraiment besoin pour avoir le contrôle total, sinon vous risquez de mettre en place un scénario de blocage, ce qui peut être très problématique à gérer.

    Voici un exemple :

     class ILockMySelf { public void doThat() { lock (this) { // Don't actually need anything here. // In this example this will never be reached. } } } class WeveGotAProblem { ILockMySelf anObjectIShouldntUseToLock = new ILockMySelf(); public void doThis() { lock (anObjectIShouldntUseToLock) { // doThat will wait for the lock to be released to finish the thread var thread = new Thread(x => anObjectIShouldntUseToLock.doThat()); thread.Start(); // doThis will wait for the thread to finish to release the lock thread.Join(); } } } 

    Vous voyez que la deuxième classe peut utiliser une instance du premier dans une instruction de locking. Cela conduit à une impasse dans l’exemple.

    L’implémentation correcte de SyncRoot est la suivante:

     object syncRoot = new object(); void doThis() { lock(syncRoot ){ ... } } void doThat() { lock(syncRoot ){ ... } } 

    Comme syncRoot est un champ privé, vous n’avez pas à vous soucier de l’utilisation externe de cet object.

    Voici une autre chose intéressante liée à ce sujet:

    Valeur douteuse de SyncRoot sur les collections (par Brad Adams) :

    Vous remarquerez une propriété SyncRoot sur de nombreuses collections de System.Collections. En rétrospection, je pense que cette propriété était une erreur. Krzysztof Cwalina, un chargé de programme de mon équipe, vient de me faire part de mes reflections sur la raison pour laquelle je suis d’accord avec lui:

    Nous avons constaté que les API de synchronisation basées sur SyncRoot n’étaient pas suffisamment flexibles pour la plupart des scénarios. Les API permettent un access sécurisé à un seul membre d’une collection. Le problème est qu’il existe de nombreux scénarios dans lesquels vous devez verrouiller plusieurs opérations (par exemple, supprimer un élément et en append un autre). En d’autres termes, c’est généralement le code qui utilise une collection qui veut choisir (et peut réellement implémenter) la stratégie de synchronisation appropriée, pas la collection elle-même. Nous avons constaté que SyncRoot est en réalité très rarement utilisé et que, dans les cas où il est utilisé, il n’a pas beaucoup de valeur ajoutée. Dans les cas où il n’est pas utilisé, c’est juste une gêne pour les implémenteurs d’ICollection.

    Soyez assuré que nous ne ferons pas la même erreur en construisant les versions génériques de ces collections.

    Le but réel de ce modèle est d’implémenter une synchronisation correcte avec la hiérarchie des wrappers.

    Par exemple, si la classe WrapperA enveloppe une instance de ClassThanNeedsToBeSynced et que la classe WrapperB encapsule la même instance de ClassThanNeedsToBeSynced, vous ne pouvez pas verrouiller WrapperA ou WrapperB, car si vous verrouillez WrapperA, le locking sur WrappedB n’attendra pas. Pour cette raison, vous devez verrouiller wrapperAInst.SyncRoot et wrapperBInst.SyncRoot, qui délèguent le verrou à celui de ClassThanNeedsToBeSynced.

    Exemple:

     public interface ISynchronized { object SyncRoot { get; } } public class SynchronizationCriticalClass : ISynchronized { public object SyncRoot { // you can return this, because this class wraps nothing. get { return this; } } } public class WrapperA : ISynchronized { ISynchronized subClass; public WrapperA(ISynchronized subClass) { this.subClass = subClass; } public object SyncRoot { // you should return SyncRoot of underlying class. get { return subClass.SyncRoot; } } } public class WrapperB : ISynchronized { ISynchronized subClass; public WrapperB(ISynchronized subClass) { this.subClass = subClass; } public object SyncRoot { // you should return SyncRoot of underlying class. get { return subClass.SyncRoot; } } } // Run class MainClass { delegate void DoSomethingAsyncDelegate(ISynchronized obj); public static void Main(ssortingng[] args) { SynchronizationCriticalClass rootClass = new SynchronizationCriticalClass(); WrapperA wrapperA = new WrapperA(rootClass); WrapperB wrapperB = new WrapperB(rootClass); // Do some async work with them to test synchronization. //Works good. DoSomethingAsyncDelegate work = new DoSomethingAsyncDelegate(DoSomethingAsyncCorrectly); work.BeginInvoke(wrapperA, null, null); work.BeginInvoke(wrapperB, null, null); // Works wrong. work = new DoSomethingAsyncDelegate(DoSomethingAsyncIncorrectly); work.BeginInvoke(wrapperA, null, null); work.BeginInvoke(wrapperB, null, null); } static void DoSomethingAsyncCorrectly(ISynchronized obj) { lock (obj.SyncRoot) { // Do something with obj } } // This works wrong! obj is locked but not the underlaying object! static void DoSomethingAsyncIncorrectly(ISynchronized obj) { lock (obj) { // Do something with obj } } } 

    Voir l’article de Jeff Richter. Plus précisément, cet exemple qui démontre que le locking sur “this” peut provoquer un blocage:

     using System; using System.Threading; class App { static void Main() { // Construct an instance of the App object App a = new App(); // This malicious code enters a lock on // the object but never exits the lock Monitor.Enter(a); // For demonstration purposes, let's release the // root to this object and force a garbage collection a = null; GC.Collect(); // For demonstration purposes, wait until all Finalize // methods have completed their execution - deadlock! GC.WaitForPendingFinalizers(); // We never get to the line of code below! Console.WriteLine("Leaving Main"); } // This is the App type's Finalize method ~App() { // For demonstration purposes, have the CLR's // Finalizer thread attempt to lock the object. // NOTE: Since the Main thread owns the lock, // the Finalizer thread is deadlocked! lock (this) { // Pretend to do something in here... } } } 

    Un autre exemple concret:

     class Program { public class Test { public ssortingng DoThis() { lock (this) { return "got it!"; } } } public delegate ssortingng Something(); static void Main(ssortingng[] args) { var test = new Test(); Something call = test.DoThis; //Holding lock from _outside_ the class IAsyncResult async; lock (test) { //Calling method on another thread. async = call.BeginInvoke(null, null); } async.AsyncWaitHandle.WaitOne(); ssortingng result = call.EndInvoke(async); lock (test) { async = call.BeginInvoke(null, null); async.AsyncWaitHandle.WaitOne(); } result = call.EndInvoke(async); } } 

    Dans cet exemple, le premier appel réussira, mais si vous tracez dans le débogueur, l’appel à DoSomething sera bloqué jusqu’à ce que le verrou soit libéré. Le second appel sera bloqué, car le thread principal maintient le verrou du moniteur en test .

    Le problème est que Main peut verrouiller l’instance d’object, ce qui signifie qu’elle peut empêcher l’instance de faire tout ce que l’object pense devoir être synchronisé. Le fait est que l’object lui-même sait ce qui nécessite un locking, et que l’interférence extérieure ne demande que des problèmes. C’est pourquoi le fait d’avoir une variable membre privée que vous pouvez utiliser exclusivement pour la synchronisation sans avoir à vous soucier des interférences externes.

    La même chose vaut pour le modèle statique équivalent:

     class Program { public static class Test { public static ssortingng DoThis() { lock (typeof(Test)) { return "got it!"; } } } public delegate ssortingng Something(); static void Main(ssortingng[] args) { Something call =Test.DoThis; //Holding lock from _outside_ the class IAsyncResult async; lock (typeof(Test)) { //Calling method on another thread. async = call.BeginInvoke(null, null); } async.AsyncWaitHandle.WaitOne(); ssortingng result = call.EndInvoke(async); lock (typeof(Test)) { async = call.BeginInvoke(null, null); async.AsyncWaitHandle.WaitOne(); } result = call.EndInvoke(async); } } 

    Utilisez un object statique privé pour synchroniser, pas le type.