thread que lock for ejemplo c# multithreading concurrency synchronization

que - synchronized c#



Bloquear en C# (7)

¿Dirty lee?

Todavía estoy un poco confuso y cuándo ajustar un bloqueo alrededor de algún código. Mi regla general es ajustar una operación en un bloqueo cuando lee o escribe en una variable estática. Pero cuando una variable estática SOLAMENTE se lee (por ejemplo, es un solo de lectura que se establece durante la inicialización del tipo), acceder a ella no tiene que estar envuelto en una declaración de bloqueo, ¿verdad? Recientemente vi un código que se parecía al siguiente ejemplo, y me hizo pensar que puede haber algunas lagunas en mi conocimiento de subprocesos múltiples:

class Foo { private static readonly string bar = "O_o"; private bool TrySomething() { string bar; lock(Foo.objectToLockOn) { bar = Foo.bar; } // Do something with bar } }

Eso simplemente no tiene sentido para mí, ¿por qué habría problemas de concurrencia con LEER un registro?

Además, este ejemplo plantea otra pregunta. ¿Es uno de estos mejor que el otro? (Por ejemplo, el ejemplo dos sostiene el candado por menos tiempo?) Supongo que podría desarmar el MSIL ...

class Foo { private static string joke = "yo momma"; private string GetJoke() { lock(Foo.objectToLockOn) { return Foo.joke; } } }

vs.

class Foo { private static string joke = "yo momma"; private string GetJoke() { string joke; lock(Foo.objectToLockOn) { joke = Foo.joke; } return joke; } }


En cuanto a su pregunta "que es mejor", son las mismas ya que el alcance de la función no se usa para nada más.


En mi opinión, debes esforzarte mucho para no poner las variables estáticas en una posición donde necesiten ser leídas / escritas desde diferentes hilos. En este caso, son esencialmente variables globales para todos, y los globales son casi siempre una mala cosa.

Dicho esto, si coloca una variable estática en dicha posición, puede bloquearla durante una lectura, por si acaso; recuerde, puede haber otro subproceso y haber cambiado el valor durante la lectura, y si lo hace, puede terminar con datos corruptos. Las lecturas no son necesariamente operaciones atómicas a menos que usted se asegure de que estén bloqueadas. Lo mismo con las escrituras: tampoco son siempre operaciones atómicas.

Editar: Como señaló Mark, para ciertas primitivas en C # las lecturas son siempre atómicas. Pero ten cuidado con otros tipos de datos.


Leer o escribir un campo de 32 bits o más pequeño es una operación atómica en C #. No hay necesidad de bloquear el código que presentaste, por lo que puedo ver.


Me parece que el bloqueo es innecesario en su primer caso. Se garantiza que el uso del inicializador estático para inicializar la barra sea seguro para subprocesos. Como solo lee el valor, no es necesario bloquearlo. Si el valor nunca va a cambiar, nunca habrá ninguna contención, ¿por qué bloquearlo?


Si solo está escribiendo un valor en un puntero, no necesita bloquear, ya que esa acción es atómica. En general, debe bloquear cada vez que necesite realizar una transacción que implique al menos dos acciones atómicas (lecturas o escrituras) que dependan de que el estado no cambie entre el comienzo y el final.

Dicho esto, vengo del territorio de Java, donde todas las lecturas y escrituras de variables son acciones atómicas. Otras respuestas aquí sugieren que .NET es diferente.


Como ninguno de los códigos que ha escrito modifica el campo estático después de la inicialización, no es necesario ningún bloqueo. El solo reemplazo de la cadena con un nuevo valor tampoco necesitará sincronización, a menos que el nuevo valor dependa de los resultados de una lectura del valor anterior.

Los campos estáticos no son los únicos que necesitan sincronización, cualquier referencia compartida que se pueda modificar es vulnerable a los problemas de sincronización.

class Foo { private int count = 0; public void TrySomething() { count++; } }

Puede suponer que dos subprocesos que ejecutan el método TrySomething estarían bien. Pero no lo es.

  1. El subproceso A lee el valor del recuento (0) en un registro para que pueda incrementarse.
  2. ¡Cambio de contexto! El programador de subprocesos decide que el subproceso A tuvo suficiente tiempo de ejecución. El siguiente en la línea es el hilo B.
  3. El hilo B lee el valor del conteo (0) en un registro.
  4. El hilo B incrementa el registro.
  5. El hilo B guarda el resultado (1) para contar.
  6. Contexto cambia de nuevo a A.
  7. El subproceso A vuelve a cargar el registro con el valor del recuento (0) guardado en su pila.
  8. El hilo A incrementa el registro.
  9. El subproceso A guarda el resultado (1) para contar.

Entonces, aunque llamamos a count ++ dos veces, el valor del conteo acaba de pasar de 0 a 1. Permite que el código sea seguro para subprocesos:

class Foo { private int count = 0; private readonly object sync = new object(); public void TrySomething() { lock(sync) count++; } }

Ahora cuando se interrumpe el hilo A, el hilo B no puede interferir con el conteo porque golpeará la instrucción de bloqueo y luego bloqueará hasta que el hilo A haya liberado la sincronización.

Por cierto, hay una forma alternativa de hacer que el incremento de Int32s e Int64s sea seguro para subprocesos:

class Foo { private int count = 0; public void TrySomething() { System.Threading.Interlocked.Increment(ref count); } }

Con respecto a la segunda parte de su pregunta, creo que elegiría cualquiera que sea más fácil de leer, cualquier diferencia de rendimiento será insignificante. La optimización temprana es la raíz de todo mal, etc.

Por qué enhebrar es difícil