2010-01-25 20 views
12

A menudo, cuando quiero una clase que es seguro para subprocesos, hago algo como lo siguiente:¿Es este un buen diseño para crear clases Thread-Safe en C#?

public class ThreadSafeClass 
{ 
    private readonly object theLock = new object(); 

    private double propertyA; 
    public double PropertyA 
    { 
     get 
     { 
      lock (theLock) 
      { 
       return propertyA; 
      } 
     } 
     set 
     { 
      lock (theLock) 
      { 
       propertyA = value; 
      } 
     } 
    } 

    private double propertyB; 
    public double PropertyB 
    { 
     get 
     { 
      lock (theLock) 
      { 
       return propertyB; 
      } 
     } 
     set 
     { 
      lock (theLock) 
      { 
       propertyB = value; 
      } 
     } 
    } 

    public void SomeMethod() 
    { 
     lock (theLock) 
     { 
      PropertyA = 2.0 * PropertyB; 
     } 
    } 
} 

Funciona, pero es muy detallado. Algunas veces, incluso creo un objeto de bloqueo para cada método y propiedad, creando más detalle y complejidad.

Sé que también es posible bloquear clases utilizando el atributo de Sincronización, pero no estoy seguro de qué tan bien se escala, ya que a menudo espero tener cientos de miles, sino millones, de instancias de seguridad de subprocesos objetos. Este enfoque crearía un contexto de sincronización para cada instancia de la clase, y requiere que la clase se derive de ContextBoundObject y, por lo tanto, no pueda derivarse de ninguna otra cosa, ya que C# no permite la herencia múltiple, que es un show stopper en muchos casos.

Editar: Como varios de los que respondieron han enfatizado, no hay un diseño de clase de subprocesos "de bala de plata". Solo trato de entender si el patrón que uso es una de las mejores soluciones. Por supuesto, la mejor solución en cualquier situación particular depende del problema. Varias de las respuestas a continuación contienen diseños alternativos que deben considerarse.

Editar: Además, hay más de una definición de seguridad de subprocesos. Por ejemplo, en mi aplicación anterior, el siguiente código no sería seguro para subprocesos:

var myObject = new ThreadSafeClass(); 
myObject.PropertyA++; // NOT thread-safe 

Entonces, ¿la definición de clase superior representan un buen enfoque? Si no, ¿qué recomendarías para un diseño con un comportamiento similar que sería seguro para subprocesos para un conjunto similar de usos?

+4

Busque primero: http://stackoverflow.com/search?q=What+is+la+máxima+práctica+para+crear+Thread-Safe+Classes+en+C%23 –

+0

Hm. En su código, usa el mismo objeto de bloqueo para leer PropertyA y PropertyB. Eso significa que un hilo que lea PropertyA bloqueará a otro tratando de leer PropertyB hasta que se complete el primer hilo. ¿Es eso lo que quieres? Si esta fuera mi clase, crearía un casillero de lectura separado para cada comprador y luego los instaladores también deben respetar ese bloqueo. Vea una respuesta a continuación. –

+0

Acabo de descalificarlo porque no hay una sola forma de producir una clase segura para subprocesos. Cualquier respuesta a esta pregunta corre el riesgo de dar la impresión equivocada a futuros lectores. –

Respuesta

8

No existe una solución de "talla única para todos" para el problema de subprocesos múltiples. Investiga un poco sobre la creación de clases inmutables y aprende sobre las diferentes primitivas de sincronización.

Este es un ejemplo de una clase semi-inmutable o los programadores-inmutable.

public class ThreadSafeClass 
{ 
    public double A { get; private set; } 
    public double B { get; private set; } 
    public double C { get; private set; } 

    public ThreadSafeClass(double a, double b, double c) 
    { 
     A = a; 
     B = b; 
     C = c; 
    } 

    public ThreadSafeClass RecalculateA() 
    { 
     return new ThreadSafeClass(2.0 * B, B, C); 
    } 
} 

este ejemplo se mueve el código de sincronización en otra clase y serializa el acceso a una instancia.En realidad, no desea realmente más de un hilo operativo en un objeto en un momento dado.

public class ThreadSafeClass 
{ 
    public double PropertyA { get; set; } 
    public double PropertyB { get; set; } 
    public double PropertyC { get; set; } 

    private ThreadSafeClass() 
    { 

    } 

    public void ModifyClass() 
    { 
     // do stuff 
    } 

    public class Synchronizer 
    { 
     private ThreadSafeClass instance = new ThreadSafeClass(); 
     private readonly object locker = new object(); 

     public void Execute(Action<ThreadSafeClass> action) 
     { 
      lock (locker) 
      { 
       action(instance); 
      } 
     } 

     public T Execute<T>(Func<ThreadSafeClass, T> func) 
     { 
      lock (locker) 
      { 
       return func(instance); 
      } 
     } 
    } 
} 

Aquí hay un ejemplo rápido de cómo lo usaría. Puede parecer un poco torpe pero te permite ejecutar muchas acciones en la instancia de una vez.

var syn = new ThreadSafeClass.Synchronizer(); 

syn.Execute(inst => { 
    inst.PropertyA = 2.0; 
    inst.PropertyB = 2.0; 
    inst.PropertyC = 2.0; 
}); 

var a = syn.Execute<double>(inst => { 
    return inst.PropertyA + inst.PropertyB; 
}); 
+0

Entiendo su enfoque aquí, y veo que en muchos casos sería el mejor. Sin embargo, no permite cambiar A, B y C con la sintaxis de la propiedad, y creará problemas de rendimiento si las actualizaciones son muy frecuentes ya que cada actualización implica la creación de un nuevo objeto y dejar que el GC maneje el anterior. –

+0

Bueno, no para una clase como esta. Pero para cualquier objeto comercial amable, estás en lo correcto. – ChaosPandion

+1

Esto es solo un ejemplo de una clase inmutable. Si bien es técnicamente correcto decir que es "seguro para subprocesos" (en el sentido de que las interacciones con múltiples subprocesos simultáneos no causarán corrupción o mala conducta) no es un modelo para "así es como deben crearse todas las clases 'seguras para subprocesos'" . –

3

Tenga en cuenta que el término "hilo seguro" no es específico; lo que está haciendo aquí se denominará con mayor precisión "sincronización" mediante el uso de un bloqueo Monitor.

Dicho esto, la verbosidad alrededor del código sincronizado es bastante inevitable. Se podría reducir en algunos de los espacios en blanco en su ejemplo girando cosas como esta:

lock (theLock) 
{ 
    propertyB = value; 
} 

en esto:

lock (theLock) propertyB = value; 

En cuanto a si es o no es el enfoque derecho para que nos realmente necesito más información. La sincronización es solo un enfoque de "seguridad de subprocesos"; objetos inmutables, semáforos, etc. son todos mecanismos diferentes que se ajustan a diferentes casos de uso. Para el ejemplo simple que proporcionas (donde parece que intentas asegurar la atomicidad de una operación get o set), parece que has hecho las cosas correctas, pero si tu código pretende ser más de ilustración que un ejemplo, entonces las cosas pueden no ser tan simples.

0

Según mi comentario anterior - se pone un poco más peludo si desea que los lectores simultáneos permitidos, pero sólo un escritor permitidos. Tenga en cuenta que si tiene .NET 3.5, use ReaderWriterLockSlim en lugar de ReaderWriterLock para este tipo de patrón.

public class ThreadSafeClass 
{ 
    private readonly ReaderWriterLock theLock = new ReaderWriterLock(); 

    private double propertyA; 
    public double PropertyA 
    { 
     get 
     { 
      theLock.AcquireReaderLock(Timeout.Infinite); 
      try 
      { 
       return propertyA; 
      } 
      finally 
      { 
       theLock.ReleaseReaderLock(); 
      } 
     } 
     set 
     { 
      theLock.AcquireWriterLock(Timeout.Infinite); 
      try 
      { 
       propertyA = value; 
      } 
      finally 
      { 
       theLock.ReleaseWriterLock(); 
      } 
     } 
    } 

    private double propertyB; 
    public double PropertyB 
    { 
     get 
     { 
      theLock.AcquireReaderLock(Timeout.Infinite); 
      try 
      { 
       return propertyB; 
      } 
      finally 
      { 
       theLock.ReleaseReaderLock(); 
      } 
     } 
     set 
     { 
      theLock.AcquireWriterLock(Timeout.Infinite); 
      try 
      { 
       propertyB = value; 
      } 
      finally 
      { 
       theLock.ReleaseWriterLock(); 
      } 
     } 
    } 

    public void SomeMethod() 
    { 
     theLock.AcquireWriterLock(Timeout.Infinite); 
     try 
     { 
      theLock.AcquireReaderLock(Timeout.Infinite); 
      try 
      { 
       PropertyA = 2.0 * PropertyB; 
      } 
      finally 
      { 
       theLock.ReleaseReaderLock(); 
      } 
     } 
     finally 
     { 
      theLock.ReleaseWriterLock(); 
     } 
    } 
} 
+0

¿Qué pasaría si tuviera 10 propiedades en lugar de dos? –

+4

Conozca sus herramientas, el marco tiene ReaderWriterLockSlim para realizar este tipo de bloqueo: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx –

+0

Esta es precisamente la razón por la que le pregunté si quería tener propiedades completamente sincronizadas juntas o no. Todo se basa en tus necesidades. Usted intercambia flexibilidad por complejidad. La sugerencia de Chris de ReaderWriterLockSlim (o ReaderWriterLock si quieres la versión HEAVYWEIGHT) funcionará para serializar los escritores. –

1

Puede que la clase Interlocked le sea útil. Contiene varias operaciones atómicas.

1

Una cosa que podría hacer para evitar el código adicional es usar algo como PostSharp para insertar automáticamente esas declaraciones lock en su código, incluso si tuviera cientos de ellas. Todo lo que necesitaría es un atributo asociado a la clase, y la implementación del atributo que agregaría las variables de bloqueo adicionales.

4

Sé que esto podría sonar como una respuesta ** inteligente pero ... la MEJOR manera de desarrollar clases seguras de hilos es conocer realmente sobre el multihilo, sobre sus implicaciones, sus complejidades y lo que implica. No hay una bala de plata.

  • Primero you need a good reason to use it. Los hilos son una herramienta, no quieres golpear todo con tu nuevo martillo encontrado.
  • En segundo lugar, aprender acerca de los problemas de múltiples hilos ... deadlocks, race conditions, starvation y así sucesivamente
  • En tercer lugar, asegúrese de que vale la pena. Estoy hablando de beneficio/costo.
  • Finalmente ... prepárese para la eliminación de errores. Debugging multithreaded code es mucho más difícil que el código secuencial estándar anterior. Aprende algunas técnicas sobre cómo hacer eso.

Seriamente ... no intente multirreda (en los escenarios de producción quiero decir) hasta que sepa en lo que se está metiendo ... Puede ser un gran error.

Edit: Por supuesto, debe conocer las primitivas de sincronización tanto del sistema operativo como del idioma de su elección (C# en Windows en este caso, supongo).

Lo siento, no estoy dando solo el código para hacer una clase segura para los hilos. Eso es porque no existe. Una clase completamente segura de hilos probablemente sea más lenta que simplemente evitar hilos y probablemente actuará como un cuello de botella a lo que sea que estés haciendo ... deshaciendo efectivamente lo que sea que estés logrando mediante el uso de hilos.

+0

Sé que no hay una bala de plata. Tengo un montón de código de producción usando el patrón que describí anteriormente, y estoy de acuerdo con usted en que el multi-threading no debe tomarse a la ligera. Sin embargo, mi aplicación es robusta y no (actualmente) sufre bloqueos o condiciones de carrera, aunque lo ha hecho en el pasado. No digo que este patrón o sus variaciones sean la respuesta a todos los problemas de subprocesos múltiples. Solo estoy preguntando si hay una manera más elegante de obtener el comportamiento que tiene el código anterior. Nunca me pareció ideal y estoy tratando de recurrir a la inteligencia de la comunidad. –

+5

Tengo que decir que es un consejo bastante malo. Es como decir que uno debe convertirse en un maestro artesano antes de levantar una sierra. Tienes que empezar en algún lado, y este no es un mal lugar. –

+0

Tengo que estar de acuerdo con Jonathan en esto. – ChaosPandion

3

Como parece que no lo está haciendo más, aquí hay algunos análisis sobre su diseño específico.

  • ¿Deseas leer una propiedad cualquiera? Threadsafe
  • ¿Desea actualizar a una sola propiedad? Threadsafe
  • ¿Desea leer una sola propiedad y luego actualizarla en función de su valor original?Not Threadsafe

El subproceso 2 podría actualizar el valor entre la lectura y la actualización del subproceso 1.

  • ¿Desea actualizar dos propiedades relacionadas al mismo tiempo? Not Threadsafe

Podría terminar con la propiedad A con el valor del subproceso 1 y la propiedad B con el subproceso 2.

  1. Tema 1 Actualización Un
  2. Tema 2 Actualización Un
  3. Tema 1 Actualización B
  4. Tema 2 Actualización B

    • quiero leer dos propiedades relacionadas al mismo tiempo? No threadsafe

Una vez más, usted podría ser interrumpido entre la primera y la segunda lectura.

Podría continuar, pero ya entiendo. Threadsafety se basa exclusivamente en cómo planea acceder a los objetos y qué promesas debe hacer.

Cuestiones relacionadas