2009-04-08 9 views
52

Estoy leyendo un libro de C# que describe el patrón SyncRoot. Muestra¿De qué sirve el patrón SyncRoot?

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

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

y lo compara con el patrón SyncRoot:

object syncRoot = new object(); 

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

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

Sin embargo, realmente no entienden la diferencia en este caso; parece que en ambos casos solo se puede acceder a ambos métodos por un hilo a la vez.

... El libro describe ... porque el objeto de la instancia también se puede usar para el acceso sincronizado desde el exterior y no puede controlar esta forma la clase en sí, puede usar el patrón SyncRoot ¿Eh? 'objeto de la instancia'?

¿Alguien me puede decir la diferencia entre los dos enfoques anteriores?

Gracias de antemano

+0

En el segundo ejemplo, es SyncRoot() se supone que es doThat()? –

+7

"objeto de la instancia" == "instancia de la clase" Libro mal redactado. –

+0

Gracias por la edición Will Dean. Maldición copiar/pegar :) – Ryan

Respuesta

66

Si usted tiene una estructura de datos interna que desea impedir el acceso simultáneo a varios subprocesos, siempre debe asegurarse de que el objeto que está sobre el bloqueo no es pública.

El razonamiento detrás de esto es que un objeto público puede ser bloqueado por cualquier persona, y por lo tanto puede crear bloqueos porque no tiene el control total del patrón de bloqueo.

Esto significa que bloquear en this no es una opción, ya que cualquiera puede bloquear ese objeto. Del mismo modo, no debe encerrarse en algo que expone al mundo exterior.

Lo que significa que la mejor solución es usar un objeto interno, y por lo tanto la punta es simplemente usar Object.

Bloquear las estructuras de datos es algo sobre lo que realmente necesita tener control total, de lo contrario correrá el riesgo de configurar un escenario para el bloqueo, que puede ser muy problemático de manejar.

+13

OK. ¿Dónde entra 'SyncRoot'? ¿Por qué los diseñadores de .NET FCL crean '.SyncRoot'? O, para parafrasear la pregunta de Ryan, * "¿Para qué sirve el patrón SyncRoot?" * –

+0

si eso es malo, ¿por qué los diseñadores de CLR crearon * lock * keywoard en primer lugar? ¿Por qué no crearon el bloqueo de clases, cuya instancia puede colocar en el campo _syncRoot? Y eso eliminaría las estructuras de bloqueo de arriba del encabezado de cada objeto –

+0

@IanBoyd - .SyncRoot ofrece a los desarrolladores una opción explícita, siempre que estén bloqueando la colección (ya sea ella misma o su SyncRoot) para indicar si desean interactuar con cualquier bloqueo dentro del código que implementa la colección, o se garantiza que no interactuará con eso. Esto afecta principalmente al código que implementa colecciones que envuelven otras colecciones (herencia, decoradores, composites ...) No dice si esto es bueno o malo. Diciendo que la mayoría de los usuarios de colecciones no necesitan preocuparse. –

17

Aquí se muestra un ejemplo:

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(); 
     } 
    } 
} 

Ves que la segunda clase se puede utilizar una instancia de la primera en un comunicado de bloqueo. Esto lleva a un punto muerto en el ejemplo.

La aplicación SyncRoot correcta es:

object syncRoot = new object(); 

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

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

como syncRoot es un campo privado, usted no tiene que preocuparse por el uso externo de este objeto.

+5

Esto realmente no fallará, ya que se están ejecutando en el mismo hilo ... –

+1

En mi humilde opinión, este es un bloqueo simple que no se ha lanzado. nada mas. esto no es un bloqueo muerto. donde un hilo espera a otro. –

+0

Bueno, excepto que el bloqueo se libera (después de salir de ambos enunciados de bloqueo), esto no fallará. Definitivamente no es un punto muerto. – BrainSlugs83

6

Ver this Artículo de Jeff Richter. Más específicamente, este ejemplo que demuestra que el bloqueo en "este" puede causar un callejón sin salida:

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... 
     } 
    } 
} 
+0

Respuesta solo de enlace. Ejemplo que muestra que el código malicioso se puede construir para un punto muerto. Y el enlace está colgando. –

11

Aquí hay una cosa más interesante relacionado con este tema:

Questionable value of SyncRoot on Collections (by Brad Adams):

podrás observe una propiedad SyncRoot en muchas de las colecciones en System.Collections.En retrospectiva, I creo que esta propiedad fue un error. Krzysztof Cwalina, a Manger Programa de mi equipo, me acaba de enviar algunos pensamientos sobre por qué esto es - Estoy de acuerdo con él:

Hemos encontrado la API de sincronización basada en SyncRoot ser suficientemente flexibles para la mayoría de escenarios . Las API permiten el acceso seguro a thread a un único miembro de una colección . El problema es que hay numerosos escenarios en los que necesita para bloquear varias operaciones (para el ejemplo , elimine un elemento y agregue otro). En otras palabras, generalmente es el código que utiliza una colección que desea elegir (y puede realmente implementar) la política de sincronización correcta , no la colección en sí. Nos encontró que SyncRoot se utiliza realmente muy raramente y en los casos en que es utilizado, en realidad no agrega mucho valor . En los casos en que no se usa, es solo una molestia para los implementadores de ICollection.

Tenga la seguridad de que no vamos a hacer lo mismo error, ya que construimos los genéricos versiones de estas colecciones

+2

SyncRoot on Collections no es lo mismo de lo que habla este tema, por lo que el campo privado SyncRoot es tan malo como bloquearlo en "this". El tema es comparar el bloqueo en el campo privado de solo lectura con el bloqueo en "esto"; en el caso de las colecciones, el campo privado se hizo accesible a través de una propiedad pública, lo cual no es una buena práctica y puede llevar a bloqueos. – haze4real

+0

@Haze ¿Puede explicar la parte "se hizo accesible a través de una propiedad pública, que no es la mejor práctica y puede conducir a puntos muertos". ¿Cómo puede ocurrir un punto muerto aquí? – prabhakaran

+0

@prabhakaran El problema con el bloqueo en 'this', un campo público o un campo privado expuesto a través de una propiedad pública es que cualquier persona puede acceder a él, lo que puede llevar a bloqueos si no conoce la implementación. Las clases no deben diseñarse de forma tal que se requiera el conocimiento de la implementación de la clase para su uso correcto. – haze4real

1

Otro ejemplo concreto:

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); 
    } 
} 

En este ejemplo, la primera llamada tendrá éxito , pero si rastrea en el depurador, la llamada a DoSomething se bloqueará hasta que se libere el bloqueo. La segunda llamada se bloqueará, ya que el hilo principal mantiene el bloqueo del monitor en , prueba.

El problema es que Main puede bloquear la instancia del objeto, lo que significa que puede evitar que la instancia haga algo que el objeto crea que debería sincronizarse. El punto es que el objeto en sí mismo sabe lo que requiere el bloqueo, y la interferencia externa es solo la búsqueda de problemas. Es por eso que el patrón de tener una variable de miembro privado que puede usar exclusivamente para la sincronización sin tener que preocuparse por la interferencia exterior.

Lo mismo ocurre con el patrón estática equivalente:

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); 
    } 
} 

con un objeto estático privado para sincronizar el, no el tipo.

9

El propósito real de este patrón es implementar la sincronización correcta con la jerarquía de envolturas.

Por ejemplo, si la clase WrapperA envuelve una instancia de ClassThanNeedsToBeSynced y WrapperB clase envuelve la misma instancia de ClassThanNeedsToBeSynced, no se puede bloquear en WrapperA o WrapperB, ya que si se bloquea en WrapperA, cerradura de WrappedB no esperará . Por este motivo, debe bloquear en wrapperAInst.SyncRoot y wrapperBInst.SyncRoot, que delegan el bloqueo en uno de ClassThanNeedsToBeSynced.

Ejemplo:

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 
     } 
    } 
} 
+0

¡Gracias por responder la pregunta! – Govert