thread synlock lock example ejemplo async c# multithreading locking reference

c# - synlock - ¿Por qué no podemos bloquear un tipo de valor?



thread synchronization c# (9)

Creo que este es uno de esos casos donde la respuesta a por qué es "porque un ingeniero de Microsoft lo implementó de esa manera".

La forma en que funciona el bloqueo debajo del capó es creando una tabla de estructuras de bloqueo en la memoria y luego usando los objetos vtable para recordar la posición en la tabla donde se encuentra el bloqueo requerido. Esto da la apariencia de que cada objeto tiene un bloqueo cuando en realidad no es así. Solo aquellos que han sido bloqueados lo hacen. Como los tipos de valor no tienen una referencia, no hay vtable para almacenar la posición de los bloqueos.

No se sabe por qué Microsoft eligió esta forma extraña de hacer las cosas. Podrían haber hecho del Monitor una clase que tuviste que instanciar. Estoy seguro de que he visto un artículo de un empleado de MS que dice que, en la reflexión, este patrón de diseño fue un error, pero parece que no puedo encontrarlo ahora.

Estaba intentando lock una variable Boolean cuando encontré el siguiente error:

''bool'' no es un tipo de referencia como lo requiere la instrucción de bloqueo

Parece que solo se permiten los tipos de referencia en las declaraciones de lock , pero no estoy seguro de entender por qué.

Andreas afirma en su comment :

Cuando el objeto [tipo de valor] se pasa de un hilo a otro, se realiza una copia, por lo que los hilos terminan trabajando en 2 objetos diferentes, lo que es seguro.

¿Es verdad? ¿ xToTrue significa que cuando hago lo siguiente, de hecho estoy modificando dos x diferentes en el xToTrue y xToFalse ?

public static class Program { public static Boolean x = false; [STAThread] static void Main(string[] args) { var t = new Thread(() => xToTrue()); t.Start(); // ... xToFalse(); } private static void xToTrue() { Program.x = true; } private static void xToFalse() { Program.x = false; } }

(este código solo es claramente inútil en su estado, es solo para el ejemplo)

PD: Conozco esta pregunta sobre comment . Mi pregunta no está relacionada con el cómo sino con el por qué .


De acuerdo con este subproceso de MSDN , los cambios a una variable de referencia pueden no ser visibles para todos los subprocesos y pueden terminar utilizando valores obsoletos, y AFAIK creo que los tipos de valor hacen una copia cuando se pasan entre subprocesos.

Para citar exactamente de MSDN

También es importante aclarar que el hecho de que la asignación sea atómica no implica que la escritura sea observada inmediatamente por otros hilos. Si la referencia no es volátil, es posible que otro hilo lea un valor obsoleto de la referencia algún tiempo después de que su hilo lo haya actualizado. Sin embargo, se garantiza que la actualización sea atómica (no verás que se actualice una parte del puntero subyacente).


Lo siguiente está tomado de MSDN:

Las instrucciones lock (C #) y SyncLock (Visual Basic) se pueden usar para garantizar que un bloque de código se ejecute sin interrupción por otros hilos. Esto se logra al obtener un bloqueo de exclusión mutua para un objeto dado durante la duración del bloque de código.

y

El argumento proporcionado a la palabra clave de bloqueo debe ser un objeto basado en un tipo de referencia, y se usa para definir el alcance del bloqueo.

Supongo que esto se debe en parte a que el mecanismo de bloqueo usa una instancia de ese objeto para crear el bloqueo de exclusión mutua.


Me preguntaba por qué el equipo de .Net decidió limitar los desarrolladores y permitir que el Monitor opere solo con referencias. En primer lugar, cree que sería bueno bloquearlo contra System.Int32 lugar de definir una variable de objeto dedicada solo para fines de bloqueo, estos casilleros no suelen hacer nada más.

Pero luego parece que cualquier característica proporcionada por el lenguaje debe tener una fuerte semántica que no solo sea útil para los desarrolladores. Entonces la semántica con tipos de valor es que cada vez que aparece un tipo de valor en el código, su expresión se evalúa a un valor. Entonces, desde el punto de vista semántico, si escribimos `lock (x) ''yx es un tipo de valor primitivo, entonces es lo mismo que diríamos" bloquear un bloque de código crítico contra el valor de la variable x "que suena más que extraño, seguro :). Mientras tanto, cuando conocemos las variables de referencia en el código, pensamos "Ah, es una referencia a un objeto" e implica que la referencia se puede compartir entre bloques de código, métodos, clases e incluso hilos y procesos y, por lo tanto, puede servir como un Guardia.

En dos palabras, las variables de tipo de valor aparecen en el código solo para ser evaluadas a su valor real en cada expresión , nada más.

Supongo que ese es uno de los puntos principales.


Porque los tipos de valor no tienen el bloque de sincronización que la instrucción de bloqueo usa para bloquear un objeto. Solo los tipos de referencia llevan la sobrecarga del tipo de información, bloque de sincronización, etc.

Si coloca su tipo de referencia, ahora tiene un objeto que contiene el tipo de valor y puede bloquear ese objeto (lo espero) ya que ahora tiene la sobrecarga adicional que tienen los objetos (un puntero a un bloque de sincronización que se usa para bloquear, un puntero a la información de tipo, etc.). Sin embargo, como todo el mundo dice: si coloca un objeto en la casilla, obtendrá un objeto NUEVO cada vez que lo coloque en una caja, de modo que se bloqueará en diferentes objetos cada vez, lo que frustra por completo el objetivo de realizar un bloqueo.

Esto probablemente funcione (aunque es completamente inútil y no lo he probado)

int x = 7; object boxed = (object)x; //thread1: lock (boxed){ ... } //thread2: lock(boxed){ ... }

Mientras todos usen cajas y el objeto en caja solo se configure una vez, probablemente obtendrás un bloqueo correcto ya que estás bloqueando el objeto en caja y solo se está creando una vez. NO HAGA esto ... es solo un ejercicio de pensamiento (y quizás ni siquiera funcione, como dije, no lo he probado).

En cuanto a su segunda pregunta: No, el valor no se copia para cada hilo. Ambos subprocesos usarán el mismo booleano, pero no garantiza que los subprocesos vean el valor más nuevo para él (cuando un subproceso establece el valor, es posible que no se vuelva a escribir en la ubicación de memoria inmediatamente, para que cualquier otro subproceso que lea el valor obtenga un resultado "viejo").


Se expande a:

System.Threading.Monitor.Enter(x); try { ... } finally { System.Threading.Monitor.Exit(x); }

Aunque compilarían, Monitor.Enter / Exit requiere un tipo de referencia porque un tipo de valor se encasillaría en una instancia de objeto diferente cada vez, de modo que cada llamada a Enter y Exit estaría operando en diferentes objetos.

Desde la página de método Enter de MSDN:

Use Monitor para bloquear objetos (es decir, tipos de referencia), no tipos de valores. Cuando pasa una variable de tipo de valor a Ingresar, se encasilla como un objeto. Si pasa la misma variable a Ingresar nuevamente, se coloca en un recuadro como un objeto separado y el hilo no se bloquea. En este caso, el código que Monitor supuestamente protege no está protegido. Además, cuando pasa la variable a Salir, se crea otro objeto separado. Como el objeto pasado a Exit es diferente del objeto pasado a Enter, Monitor lanza SynchronizationLockException. Para obtener más información, consulte el tema conceptual Monitores.


Si está preguntando conceptualmente por qué esto no está permitido, diría que la respuesta se debe al hecho de que la identidad de un tipo de valor es exactamente equivalente a su valor (eso es lo que lo convierte en un tipo de valor).

Entonces, cualquier persona en el universo que hable sobre el int 4 está hablando de lo mismo : ¿cómo puede entonces reclamar acceso exclusivo para bloquearlo?


Solo una conjetura salvaje aquí ...

pero si el compilador le permitiera bloquear un tipo de valor, terminaría bloqueando nada ... porque cada vez que pasó el tipo de valor al lock , estaría pasando una copia en caja del mismo; una copia en caja diferente. Entonces los bloqueos serían como si fueran objetos completamente diferentes. (ya que, en realidad lo son)

Recuerde que cuando pasa un tipo de valor para un parámetro de tipo object , se encuadra (envuelve) en un tipo de referencia. Esto lo convierte en un objeto nuevo cada vez que esto sucede.


No puede bloquear un tipo de valor porque no tiene un registro sync root .

El bloqueo se lleva a cabo mediante mecanismos internos de CLR y OS que se basan en un objeto que tiene un registro al que solo se puede acceder mediante un único hilo a la vez: raíz de bloque de sincronización. Cualquier tipo de referencia tendría:

  • Puntero a un tipo
  • Sincronizar raíz de bloque
  • Puntero a los datos de instancia en el montón