2008-09-19 16 views
14

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 archivo de solo lectura que se configura durante la inicialización del tipo), acceder a ella no tiene que estar envuelto en una declaración de bloqueo, ¿verdad? Hace poco vi un código que parecía el siguiente ejemplo, y me hizo pensar que puede haber algunas lagunas en mi conocimiento multihilo:Bloqueo en C#

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 de concurrencia problemas con LEER un registro?

Además, este ejemplo plantea otra pregunta. ¿Es uno de estos mejor que el otro? (Por ejemplo, Ejemplo de dos mantiene el bloqueo por menos tiempo?) Supongo que podría desmontar 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; 
     } 
} 

Respuesta

23

Dado que ninguno de los códigos que ha escrito modifica el campo estático después de la inicialización, no hay necesidad de 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 pueda modificarse es vulnerable a 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 de conteo (0) en un registro para que pueda ser incrementado.
  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 subproceso B incrementa el registro.
  5. El hilo B guarda el resultado (1) para contar.
  6. Cambio de contexto a A.
  7. El subproceso A vuelve a cargar el registro con el valor del recuento (0) guardado en su pila.
  8. El subproceso A incrementa el registro.
  9. El subproceso A guarda el resultado (1) para contar.

Así, a pesar de que hemos llamado recuento ++ dos veces, el valor de recuento se acaba de ir 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 subproceso A se interrumpe Tema B no se puede meter con la cuenta porque golpeará la instrucción de bloqueo y luego bloqueará hasta que el hilo A haya liberado la sincronización.

Por cierto, no es una forma alternativa de hacer incrementar Int32s y Int64s flujos seguros:

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

En cuanto a la segunda parte de su pregunta, creo que me gustaría ir con el que sea más fácil de leer , cualquier diferencia de rendimiento será insignificante. optimización temprana es la raíz de todo mal, etc.

Why threading is hard

+1

"La optimización temprana es la raíz de todo mal, etc.". Pensé que era 'optimización prematura', la optimización temprana es buena si viene después de las mediciones, es prematuro ... es otra cosa y generalmente sin mediciones en absoluto ... –

+0

Creo que "temprano" significa "prematuro" en el contexto de La frase. –

+0

Estoy un poco confundido aquí, pensé que cada hilo tiene su * propio * espacio de pila. Como count es una int (variable de pila), seguramente este valor se aislaría a un subproceso individual, en su propia pila. – miguel

1

Si acaba de escribir un valor a un puntero, no es necesario 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 principio y el final.

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

1

¿Dirty reads?

+0

¿Qué son "sucio lee?" ¿Eso tiene algo que ver con el código inseguro/no administrado? – core

+0

No, échele un vistazo al artículo de Wikipedia: http://en.wikipedia.org/wiki/Isolation_level#READ_UNCOMMITTED_.28dirty_reads.29 – noocyte

7

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.

+0

en realidad, eso es solo porque tiene una sonrisa de OS * de 32 bits * Sin embargo, estaba en Windows Mobile No estoy tan seguro de que ese sea el caso. – blowdart

+0

Sin embargo, incrementar o agregar un valor de 32 bits no es atómico, por lo que todavía debe tener cuidado. –

+0

Matt, si está incrementando o agregando un valor de 32 bits, eso sería una escritura y, por lo tanto, la frase "leer o escribir un campo de 32 bits ... es una operación atómica" de Mark sería incorrecta, ¿no? Tu declaración parece contradecir a Mark. – core

1

En mi opinión, debe esforzarse mucho para no colocar las variables estáticas en una posición donde se necesiten leer/escribir desde diferentes subprocesos. En este caso, son esencialmente variables globales para todos, y los globales son casi siempre una mala cosa.

Dicho esto, si lo hace poner una variable estática en una posición tal, es posible que desee para bloquear durante una lectura, por si acaso - recuerde, otro hilo puede haber abalanzaron y 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.

+0

¿Qué sugerirías en lugar de usar variables estáticas? Por ejemplo, tengo un ensamblado Databases.dll que lee una cadena de conexión de un archivo .config y la almacena en una clase estática para que pueda usarse para crear objetos DataContext. ¿Cuál sería una alternativa a eso, por ejemplo? – core

+0

En el ejemplo, sin embargo, ningún otro subproceso puede cambiar el valor durante la lectura, por lo que no hay contención para el campo. Entonces no hay cerradura, ¿verdad? – core

+0

Sí, yo diría que no hay bloqueo en ese caso ... Hmm. En este caso, es posible que esté de acuerdo con la variable estática, de hecho, probablemente haría lo mismo, porque es excesivo en este caso (en mi humilde opinión) hacer otra cosa. Yo diría que enfoque las variables estáticas caso por caso. –

0

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

+0

La IL para el segundo ejemplo es más corta, pero no de una manera significativa. Tienes razón. – core

3

Me parece que el bloqueo no es necesario 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?

+0

Mis pensamientos exactamente. – core