web-dev-qa-db-fra.com

Quelle est l'utilité du modèle SyncRoot?

Je lis un livre c # qui décrit le modèle SyncRoot. Ça montre

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

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

et se 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 thread à la fois.

Le livre décrit ... parce que l'objet de l'instance peut également être utilisé pour un accès synchronisé de l'extérieur et que vous ne pouvez pas contrôler ce formulaire de la classe elle-même, vous pouvez utiliser le modèle SyncRoot Eh? "objet de l'instance"?

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

63
Ryan

Si vous avez une structure de données interne à laquelle vous souhaitez empêcher l'accès simultané par plusieurs threads, vous devez toujours vous assurer que l'objet sur lequel vous verrouillez n'est pas public.

Le raisonnement derrière cela est qu'un objet public peut être verrouillé par n'importe qui, et donc vous pouvez créer des blocages parce que vous n'êtes pas en contrôle total du modèle de verrouillage.

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

Ce qui signifie que la meilleure solution est d'utiliser un objet interne, et donc l'astuce est d'utiliser simplement Object.

Le verrouillage des structures de données est quelque chose dont vous avez vraiment besoin d'avoir un contrôle total, sinon vous risquez de configurer un scénario de blocage, 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 de la première dans une instruction de verrouillage. Cela conduit à un blocage dans l'exemple.

L'implémentation correcte de SyncRoot est:

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

18
ybo

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

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

Vous remarquerez une propriété SyncRoot sur de nombreuses collections dans System.Collections. En rétrospective (sic), je pense que cette propriété était une erreur. Krzysztof Cwalina, responsable de programme au sein de mon équipe, vient de m'envoyer quelques réflexions sur les raisons de cela - 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 accès sécurisé aux threads à 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 ajouter 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 bonne stratégie de synchronisation, pas la collection elle-même. Nous avons constaté que SyncRoot est en fait très rarement utilisé et dans les cas où il est utilisé, il n'ajoute en fait pas beaucoup de valeur. Dans les cas où il n'est pas utilisé, c'est juste une gêne pour les implémenteurs de ICollection.

Soyez assurés que nous ne commettrons pas la même erreur en créant les versions génériques de ces collections.

13
Igor Brejc

Le véritable objectif de ce modèle est d'implémenter une synchronisation correcte avec la hiérarchie des wrappers.

Par exemple, si la classe WrapperA encapsule 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 verrouillage sur WrappedB n'attendra pas. Pour cette raison, vous devez verrouiller sur wrapperAInst.SyncRoot et wrapperBInst.SyncRoot, qui délèguent le verrouillage à 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(string[] 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
        }
    }
}
12
Roman Zavalov

Voir ceci l'article de Jeff Richter. Plus précisément, cet exemple qui démontre que le verrouillage 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...
      }
   }
}
6
Anton Gogolev

Un autre exemple concret:

class Program
{
    public class Test
    {
        public string DoThis()
        {
            lock (this)
            {
                return "got it!";
            }
        }
    }

    public delegate string Something();

    static void Main(string[] 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();
        string 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 se bloquera jusqu'à ce que le verrou soit libéré. Le deuxième appel se bloquera, car le thread principal maintient le verrouillage du moniteur test.

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

Il en va de même pour le modèle statique équivalent:

class Program
{
    public static class Test
    {
        public static string DoThis()
        {
            lock (typeof(Test))
            {
                return "got it!";
            }
        }
    }

    public delegate string Something();

    static void Main(string[] 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();
        string result = call.EndInvoke(async);

        lock (typeof(Test))
        {
            async = call.BeginInvoke(null, null);
            async.AsyncWaitHandle.WaitOne();
        }
        result = call.EndInvoke(async);
    }
}

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

2
Darren Clark