2012-05-14 18 views
5

He estado preguntando a mí mismo: "¿Por qué usar la cerradura a una sola declaración" ...bloqueando solo 1 operación?

(en mi humilde opinión - si su operación sólo 1 como una misión - por lo que no debería ser un problema ..)?

entonces vi esto:

Como regla básica, que necesita para bloquear todo acceso a cualquier permiso de escritura compartida campo. Incluso en el caso más simple, una operación de asignación en un solo campo , debe tener en cuenta la sincronización. En la clase siguiente, ni el incremento ni el método de asignación es seguro para subprocesos:

class ThreadUnsafe 
{ 
    static int _x; 
    static void Increment() { _x++; } 
    static void Assign() { _x = 123; } 
} 

¿Puede usted decirme por qué esto no es hilo de seguridad? he estado ejecutando muchos scripts en mi cabeza y no pude encontrar ningún problema ...

+0

"Una operación" suele ser "unas pocas operaciones". Observe el desmontaje al depurar para ver el ensamblaje de su código y comprobar si realmente se trata de una operación. Si es así, no es necesario bloquearlo, obviamente. Aunque no veo por qué alguna vez tendrá que bloquear una operación, lógicamente. – SimpleVar

+1

'Incremento' no es seguro (3 operaciones). 'Assign' tampoco es seguro (1 operación atómica, pero debe tener en cuenta la sincronización de memoria a través de cachés). –

+1

@Soohjun Incremento es una operación atómica (INC dword ptr [ebp-3Ch]) – SimpleVar

Respuesta

4

Aquí hay un ejemplo de por qué su ejemplo no es seguro para subprocesos. Inicialmente, _x = 0. Supongamos que ejecuta Increment y Assign en paralelo. Si los métodos eran seguros para subprocesos, el resultado debería ser 100 (si el incremento se ejecuta antes de la asignación) o 101 (si el incremento se ejecuta después de la asignación).

(EDIT: Tenga en cuenta que cada hilo tiene su propia pila de trabajo!)

Thread 1 (executing Increment) Thread 2 (executing Assign 100) 
----------------------------------------------------------------- 
read _x onto stack  (= 0) 
            put 100 on top of stack 
            write top of stack to _x (= 100) 
increment top of stack (= 1) 
write top of stack to _x (= 1) 

_x es ahora 1, que no es ni 100 ni 101.

Por supuesto, es posible que su método de incremento esté compilado en una sola operación atómica por el compilador. Pero no puede confiar en esto, a menos que esté específicamente garantizado por el compilador que utiliza.


Si utiliza una cerradura, ocurre lo siguiente:

Thread 1 (executing Increment) Thread 2 (executing Assign 100) 
----------------------------------------------------------------- 
lock (success) 
read _x onto stack  (= 0) 
            lock (lock already taken; 
            |  wait until Thead 1's lock is released) 
increment top of stack (= 1) | 
write top of stack to _x (= 1) | 
unlock       | 
            +> (success) 
            put 100 on top of stack 
            write top of stack to _x (= 100) 
            unlock 

El resultado es ahora 100. Básicamente, el bloqueo garantiza que dos bloques bloqueados no se superpongan.

+2

¿cómo se puede incrementar be = 1? el segundo hilo establece 100 .... ??? –

+0

I segundo @RoyiNamir. Incrementar resultará en 101, donde ESPERABA que sea 1, que básicamente sería el problema. – SimpleVar

+0

@RoyiNamir: He extendido el ejemplo para hacerlo más claro. Cada hilo tiene su propia pila de trabajo. – Heinzi

1

Tiene que pensar en un nivel aún más bajo que el del lenguaje de programación.

No hay ninguna garantía de que

a) El procesador escribirá el nuevo valor de una sola vez (atómica o no atómico)

b) El valor se actualizará en la memoria caché de un núcleo de CPU, pero no en otro (falta de barreras de memoria)

Tal vez su CPU (probablemente) pueda leer y escribir un entero de 32 bits atómicamente y no tendrá ningún problema. Pero, ¿qué ocurre cuando intentas leer/escribir un valor de 64 bits? A 128? El valor podría terminar en un estado intermedio en el que dos subprocesos diferentes están modificando simultáneamente la misma ubicación de memoria, y terminará con valor a, valor b o un valor intermedio (y muy incorrecto) que es una mezcla de los dos.

y muchos más.

+0

Actually C# garantiza que los asignados a Int32 * son * atómicos. –

+0

De acuerdo con la especificación de referencia, las actualizaciones int son atómicas, sin embargo, existe la posibilidad remota de registrar el almacenamiento en caché. –

+0

@Soohjun pero no necesariamente a través de barreras de memoria, ¿no? Eso deja el problema de la coherencia del caché. –

4

La operación de incremento produce este MSIL ...

.method private hidebysig static void Increment() cil managed 
{ 
    // Code size  14 (0xe) 
    .maxstack 8 
    IL_0000: nop 
    IL_0001: ldsfld  int32 ThreadUnsafe::_x 
    IL_0006: ldc.i4.1 
    IL_0007: add 
    IL_0008: stsfld  int32 ThreadUnsafe::_x 
    IL_000d: ret 
} // end of method ThreadUnsafe::Increment 

Así se puede ver que, incluso en el nivel de MSIL, el incremento no es atómico. El compilador JIT podría posiblemente hacer algo inteligente para convertir esto de nuevo en un incremento atómico a nivel de máquina, pero ciertamente no podemos depender de eso. Imagine 2 subprocesos incrementando la misma X con sus operaciones de "carga" y "tienda" superpuestas - puede ver que es posible terminar con X = X + 1 en lugar de X + 2.

Envolviendo su incremento dentro de un bloqueo significa que no se pueden superponer.

0

El bloqueo es un gran tema desordenado, por lo general, te será muy difícil descifrar qué sucede debajo del capó (qué memoria caché central se invalida cuando). Es por eso que escribir un código paralelo eficiente es un problema. Otros han señalado algunos problemas potenciales incluso con una sola tarea (y obviamente con el incremento de una variable). Sólo hay que ver todos los problemas con volatile palabra clave: https://www.google.com/search?q=.net+volatile+concurrency&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:official&client=firefox-a

Por lo tanto, si tiene que hacer las cosas de forma paralela, se inicia mediante el bloqueo mucho, incluso en operaciones que no cree que requieren cerraduras. Optimice su bloqueo solo cuando vea problemas de rendimiento.

Cuestiones relacionadas