2009-02-02 11 views
42

Esta es una pregunta de detalle para C#.Seguridad de hilo C# con get/set

Supongamos que tengo una clase con un objeto, y ese objeto está protegido por una cerradura:

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     return property; 
    } 
    set { 
     property = value; 
    } 
} 

Quiero un hilo de votación para ser capaz de consultar esa propiedad. También quiero que el subproceso actualice propiedades de ese objeto ocasionalmente, y algunas veces el usuario puede actualizar esa propiedad, y el usuario desea poder ver esa propiedad.

¿El siguiente código bloqueará correctamente los datos?

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     lock (mLock){ 
      return property; 
     } 
    } 
    set { 
     lock (mLock){ 
       property = value; 
     } 
    } 
} 

Por 'adecuadamente', lo que quiero decir es, si quiero llamar

MyProperty.Field1 = 2; 

o lo que sea, será el campo puede bloquear mientras hago la actualización? Es la configuración realizada por el operador igual dentro del alcance de la función 'get', o la función 'get' (y por lo tanto el bloqueo) terminará primero, y luego se llamará a la configuración, y luego 'set', evitando así ¿La cerradura?

Editar: Dado que aparentemente esto no funciona, ¿qué será? ¿Necesito hacer algo como:

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     MyObject tmp = null; 
     lock (mLock){ 
      tmp = property.Clone(); 
     } 
     return tmp; 
    } 
    set { 
     lock (mLock){ 
       property = value; 
     } 
    } 
} 

que más o menos solo se asegura de que sólo tengo acceso a una copia, lo que significa que si yo fuera a tener dos hilos llaman un 'get', al mismo tiempo, cada uno comenzaría con el mismo valor de Field1 (¿verdad?). ¿Hay alguna forma de hacer bloqueo de lectura y escritura en una propiedad que tenga sentido? ¿O debería limitarme a bloquear secciones de funciones en lugar de los datos en sí?

Solo para que este ejemplo tenga sentido: MyObject es un controlador de dispositivo que devuelve el estado de forma asincrónica. Le envío comandos a través de un puerto serie, y luego el dispositivo responde a esos comandos en su propio momento. En este momento, tengo un hilo que lo sondea por su estado ("¿Sigues ahí? ¿Puedes aceptar comandos?"), Un hilo que espera respuestas en el puerto serial ("Acabo de recibir la cadena de estado 2, todo está bien"), y luego el subproceso UI que toma otros comandos ("El usuario quiere que hagas esto") y publica las respuestas del controlador ("Acabo de hacer el trabajo, ahora actualizo la interfaz de usuario con eso"). Es por eso que quiero asegurar el objeto en sí mismo, en lugar de los campos del objeto; eso sería una gran cantidad de bloqueos, a, y b, no todos los dispositivos de esta clase tienen el mismo comportamiento, solo el comportamiento general, así que tendría que codificar muchos diálogos individuales si individualizara los bloqueos.

Respuesta

34

No, su código no bloqueará el acceso a los miembros del objeto devuelto por MyProperty. Solo bloquea MyProperty.

Su ejemplo de uso es realmente dos operaciones, todo en uno, más o menos equivalente a esto:

// object is locked and then immediately released in the MyProperty getter 
MyObject o = MyProperty; 

// this assignment isn't covered by a lock 
o.Field1 = 2; 

// the MyProperty setter is never even called in this example 

En pocas palabras - si dos subprocesos tienen acceso MyProperty al mismo tiempo, el comprador va a bloquear brevemente el segundo hilo hasta que vuelve la Objeto al primer hilo, pero, luego devolverá el objeto al segundo hilo también. Ambos hilos tendrán acceso completo y desbloqueado al objeto.

EDITAR en respuesta a información más detallada en la pregunta

todavía no estoy 100% seguro de lo que estamos tratando de lograr, pero si lo que desea el acceso al objeto atómica entonces ¿No podrías tener el bloqueo del código de llamada contra el objeto mismo?

// quick and dirty example 
// there's almost certainly a better/cleaner way to do this 
lock (MyProperty) 
{ 
    // other threads can't lock the object while you're in here 
    MyProperty.Field1 = 2; 
    // do more stuff if you like, the object is all yours 
} 
// now the object is up-for-grabs again 

No es ideal, pero siempre y cuando todo el acceso al objeto está contenido en lock (MyProperty) secciones a continuación, este enfoque será thread-safe.

+0

De acuerdo. Proporcioné una solución de muestra para lo que está preguntando en respuesta a una pregunta de Singleton http://stackoverflow.com/questions/7095/is-the-c-static-constructor-thread-safe/7105#7105 – Zooba

+0

sí, me ' Probablemente vaya con bloquear el objeto como lo ha descrito. Gracias. – mmr

+0

Buen punto, sin embargo, si tenía un 'MyProperty' llamado' p' y llamado dos hilos a la vez: 'p = new MyObject (12)' y 'p = new MyObject (5)' entonces * este * acceso estaría sincronizado . Sin embargo, el miembro 'Field1' * * nunca * estaría bloqueado. Estoy bastante seguro de que esto es lo que dices, solo trato de entenderlo por mí mismo. – Snoopy

1

En el ejemplo de código que publicó, nunca se realiza un get.

En un ejemplo más complicado:

MyProperty.Field1 = MyProperty.doSomething() + 2; 

Y, por supuesto, asumiendo que hizo una:

lock (mLock) 
{ 
    // stuff... 
} 

En doSomething() entonces todas las llamadas de bloqueo habría no ser suficiente para garantizar la sincronización a través de el objeto completo Tan pronto como la función doSomething() regrese, el bloqueo se pierde, luego se realiza la adición, y luego ocurre la asignación, que se bloquea nuevamente.

O, para escribir de otra manera se puede pretender que los bloqueos no se hacen amutomatically, y volver a escribir esto más como "código máquina" con una operación por línea, y se hace evidente:

lock (mLock) 
{ 
    val = doSomething() 
} 
val = val + 2 
lock (mLock) 
{ 
    MyProperty.Field1 = val 
} 
+0

hunh. Tal vez estoy malinterpretando propiedades, pero el depurador ciertamente parece estar entrando en la función 'get', especialmente cuando pongo un punto de interrupción ahí, solo por una simple tarea. – mmr

+0

Admitiré que no estoy 100% seguro de que no invoque un get, pero no veo ninguna razón por la que lo haría. Si invoca un get, lo mismo que dije se aplica, simplemente reemplace doSomething() con su función get. – SoapBox

+0

Invoca un get, cada vez. De lo contrario, ¿de dónde vendría la referencia? –

1

El La belleza del multihilo es que no sabes en qué orden van a pasar las cosas. Si configuras algo en un hilo, podría suceder primero, podría suceder después del intento.

El código que ha publicado con el miembro se bloquea mientras se lee y escribe. Si desea manejar el caso donde se actualiza el valor, quizás deba buscar otras formas de sincronización, como events. (Vea las versiones automática/manual). Luego puede decirle a su hilo de "sondeo" que el valor ha cambiado y que está listo para ser releído.

2

El ámbito de bloqueo de su ejemplo está en el lugar incorrecto; debe estar en el ámbito de la propiedad de la clase 'MyObject' en lugar de en su contenedor.

Si MyObject my object class se usa simplemente para contener los datos que un thread desea escribir, y otro (the UI thread) para leer desde entonces, es posible que no necesite un setter y lo construya una vez.

Considere también si la colocación de bloqueos en el nivel de propiedad es el nivel de escritura de la granularidad de bloqueo; si se puede escribir más de una propiedad para representar el estado de una transacción (por ej .: pedidos totales y peso total), entonces podría ser mejor tener el bloqueo en el nivel MyObject (es decir, lock (myObject.SyncRoot) ... .)

0

En su versión editada, aún no proporciona una forma segura de actualizar MyObject. Cualquier cambio en las propiedades del objeto tendrá que hacerse dentro de un bloque sincronizado/bloqueado.

Puede escribir facilitadores individuales para manejar esto, pero ha indicado que esto será difícil debido a los campos de número grande.Si de hecho es el caso (y aún no ha proporcionado suficiente información para evaluar esto), una alternativa es escribir un colocador que use la reflexión; esto le permitiría pasar una cadena que representa el nombre del campo, y podría buscar dinámicamente el nombre del campo y actualizar el valor. Esto le permitiría tener un setter único que funcionaría en cualquier cantidad de campos. Esto no es tan fácil ni tan eficiente, pero le permitiría tratar con una gran cantidad de clases y campos.

12

La programación concurrente sería bastante fácil si su enfoque pudiera funcionar. Pero no es así, el iceberg que se hunde el Titanic que es, por ejemplo, el cliente de la clase haciendo esto:

objectRef.MyProperty += 1; 

La carrera de lectura-modificación-escritura es bastante obvio, hay peores. No hay absolutamente nada que pueda hacer para que su propiedad sea segura para hilos, a menos que sea inmutable. Es su cliente quien necesita lidiar con el dolor de cabeza. Ser obligado a delegar ese tipo de responsabilidad a un programador que es menos probable que lo haga bien es el talón de Aquiles de la programación concurrente.

+0

¿no sería genial si fuera así de fácil? Estoy seguro de que hay razones por las que no lo es, pero simplemente no conozco los detalles suficientes para saber por qué el código no funciona de esta manera. – mmr

+0

Es importante entender esto, no intente programación simultánea si no lo hace. Google "actualizaciones atómicas". –

+0

Entiendo la atomicidad, pero estoy confuso en que las cosas son atómicas en C#. Solo las asignaciones de tipos básicos, ¿verdad? Especificar la atomicidad con algo como Interlocked podría ser útil ... – mmr

4

Como han señalado otros, una vez que devuelve el objeto del captador, pierde el control sobre quién accede al objeto y cuándo. Para hacer lo que quieres hacer, tendrás que poner un candado dentro del objeto en sí.

Quizás no entiendo la imagen completa, pero según su descripción, no parece que necesite necesariamente tener un candado para cada campo individual. Si tiene un conjunto de campos que simplemente se leen y escriben a través de getters y setters, probablemente pueda salirse con la suya con un solo bloqueo para estos campos. Obviamente, es posible que serialice innecesariamente el funcionamiento de sus hilos de esta manera. Pero de nuevo, según su descripción, tampoco parece que esté accediendo agresivamente al objeto.

También sugiero usar un evento en lugar de usar un hilo para sondear el estado del dispositivo. Con el mecanismo de sondeo, tocará el candado cada vez que el hilo lo consulte. Con el mecanismo de evento, una vez que el estado cambia, el objeto notificaría a cualquier oyente. En ese momento, su hilo de "sondeo" (que ya no sería un sondeo) se activaría y obtendría el nuevo estado. Esto será mucho más eficiente.

A modo de ejemplo ...

public class Status 
{ 
    private int _code; 
    private DateTime _lastUpdate; 
    private object _sync = new object(); // single lock for both fields 

    public int Code 
    { 
     get { lock (_sync) { return _code; } } 
     set 
     { 
      lock (_sync) { 
       _code = value; 
      } 

      // Notify listeners 
      EventHandler handler = Changed; 
      if (handler != null) { 
       handler(this, null); 
      } 
     } 
    } 

    public DateTime LastUpdate 
    { 
     get { lock (_sync) { return _lastUpdate; } } 
     set { lock (_sync) { _lastUpdate = value; } } 
    } 

    public event EventHandler Changed; 
} 

Su hilo 'polling' sería algo como esto.

Status status = new Status(); 
ManualResetEvent changedEvent = new ManualResetEvent(false); 
Thread thread = new Thread(
    delegate() { 
     status.Changed += delegate { changedEvent.Set(); }; 
     while (true) { 
      changedEvent.WaitOne(Timeout.Infinite); 
      int code = status.Code; 
      DateTime lastUpdate = status.LastUpdate; 
      changedEvent.Reset(); 
     } 
    } 
); 
thread.Start(); 
+0

Interesante. Tendré que echarle un vistazo a esto. Mi primera impresión es que no funcionará, porque el dispositivo que estoy usando no dará un mensaje de estado a menos que se lo pregunten. Entonces, si está en modo 'prep', no hay indicación a menos que preguntes. – mmr

+0

Sí, si tiene que pinchar el dispositivo para informar el estado, es posible que la votación sea lo único que puede hacer. Por casualidad, ¿hay algún mecanismo de devolución de llamada para que el dispositivo informe el estado periódicamente? Odio las encuestas porque son ineficientes, pero a veces no tienes otra opción. –

+0

Desafortunadamente, es un verdadero dispositivo en serie, solo responde al sondeo. Esto no es USB, esto es de 9 pines. La vieja escuela retro todo el camino, supongo. – mmr

-1

¿El C# cerraduras no sufren de los mismos problemas de bloqueo como otros idiomas entonces?

E.G.

var someObj = -1; 

// Thread 1 

if (someObj = -1) 
    lock(someObj) 
     someObj = 42; 

// Thread 2 

if (someObj = -1) 
    lock(someObj) 
     someObj = 24; 

Esto podría tener el problema de que ambos hilos finalmente obtengan sus bloqueos y cambien el valor. Esto podría conducir a algunos errores extraños. Sin embargo, no desea bloquear innecesariamente el objeto a menos que lo necesite. En este caso, debe considerar el doble bloqueo verificado.

// Threads 1 & 2 

if (someObj = -1) 
    lock(someObj) 
     if(someObj = -1) 
      someObj = {newValue}; 

Solo algo para tener en cuenta.

Cuestiones relacionadas