una traves thread subprocesos subproceso que parametros para otro net llamó llamar interfaz hilos form diferente desde controles control con como clase aplicación aplanó acceder c# .net multithreading thread-safety memory-model

c# - traves - ¿Es posible observar un objeto parcialmente construido desde otro hilo?



thread c# windows form (3)

A menudo escuché que en el modelo de memoria .NET 2.0, las escrituras siempre usan cercas de liberación. ¿Es esto cierto? ¿Esto significa que incluso sin barreras de memoria o bloqueos explícitos, es imposible observar un objeto parcialmente construido (considerando solo los tipos de referencia) en un hilo diferente del que se creó? Obviamente estoy excluyendo los casos en que el constructor filtra this referencia.

Por ejemplo, digamos que teníamos el tipo de referencia inmutable:

public class Person { public string Name { get; private set; } public int Age { get; private set; } public Person(string name, int age) { Name = name; Age = age; } }

¿Sería posible con el siguiente código observar cualquier salida que no sea "John 20" y "Jack 21", diga "null 20" o "Jack 0"?

// We could make this volatile to freshen the read, but I don''t want // to complicate the core of the question. private Person person; private void Thread1() { while (true) { var personCopy = person; if (personCopy != null) Console.WriteLine(personCopy.Name + " " + personCopy.Age); } } private void Thread2() { var random = new Random(); while (true) { person = random.Next(2) == 0 ? new Person("John", 20) : new Person("Jack", 21); } }

¿Esto también significa que puedo hacer que todos los campos compartidos de tipos de referencia profundamente inmutables sean volatile y (en la mayoría de los casos) simplemente continúen con mi trabajo?


A menudo escuché que en el modelo de memoria .NET 2.0, las escrituras siempre usan cercas de liberación. ¿Es esto cierto?

Depende del modelo al que te refieras.

Primero, definamos con precisión una barrera de liberación de la cerca. La semántica de publicación estipula que ninguna otra lectura o escritura que aparezca antes de la barrera en la secuencia de instrucciones puede moverse después de esa barrera.

  • La especificación ECMA tiene un modelo relajado en el que las escrituras no proporcionan esta garantía.
  • Se ha citado en alguna parte que la implementación de CLR proporcionada por Microsoft fortalece el modelo al hacer que las escrituras tengan una semántica de lanzamiento de valla.
  • Las arquitecturas x86 y x64 fortalecen el modelo al hacer que las barreras de liberación de la escritura se escriban y se lean las barreras de la cerca de adquisición.

Por lo tanto, es posible que otra implementación de la CLI (como Mono) que se ejecuta en una arquitectura esotérica (como ARM a la que Windows 8 apuntará ahora) no proporcione una semántica de lanzamiento de vallas en las escrituras. Note que dije que es posible, pero no es cierto. Pero, entre todos los modelos de memoria en juego, como las diferentes capas de software y hardware, debe codificar el modelo más débil si desea que su código sea verdaderamente portátil. Eso significa codificar contra el modelo ECMA y no hacer suposiciones.

Deberíamos hacer una lista de las capas del modelo de memoria en juego para que sea explícita.

  • Compilador: El C # (o VB.NET o lo que sea) puede mover instrucciones.
  • Tiempo de ejecución: Obviamente, el tiempo de ejecución de CLI a través del compilador JIT puede mover instrucciones.
  • Hardware: Y, por supuesto, la CPU y la arquitectura de la memoria también entran en juego.

¿Esto significa que incluso sin barreras de memoria o bloqueos explícitos, es imposible observar un objeto parcialmente construido (considerando solo los tipos de referencia) en un hilo diferente del que se creó?

Sí (calificado): si el entorno en el que se está ejecutando la aplicación es lo suficientemente oscuro, es posible que se pueda observar una instancia parcialmente construida desde otro hilo. Esta es una de las razones por las que el patrón de bloqueo de doble verificación no sería seguro sin usar volatile . Sin embargo, en realidad, dudo que alguna vez te encuentres con esto porque la implementación de CLI por parte de Microsoft no reordena las instrucciones de esta manera.

¿Sería posible con el siguiente código observar cualquier salida que no sea "John 20" y "Jack 21", diga "null 20" o "Jack 0"?

De nuevo, eso está calificado sí. Pero por alguna razón como la anterior, dudo que alguna vez observes tal comportamiento.

Sin embargo, debo señalar que debido a que la person no está marcada como volatile , podría ser posible que no se imprima nada porque el hilo de lectura siempre lo vea como null . En realidad, sin embargo, apuesto a que la llamada de Console.WriteLine hará que los compiladores C # y JIT eviten la operación de elevación que de otra manera podría mover la lectura de una person fuera del bucle. Sospecho que ya estás al tanto de este matiz.

¿Esto también significa que puedo hacer que todos los campos compartidos de tipos de referencia profundamente inmutables sean volátiles y (en la mayoría de los casos) seguir con mi trabajo?

No lo sé. Esa es una pregunta bastante cargada. No me siento cómodo respondiendo de ninguna manera sin una mejor comprensión del contexto detrás de él. Lo que puedo decir es que normalmente evito el uso de volatile en lugar de instrucciones de memoria más explícitas, como las operaciones Thread.VolatileRead , Thread.VolatileWrite , Thread.MemoryBarrier y Thread.MemoryBarrier . Por otra parte, también trato de evitar el código de no bloqueo por completo a favor de los mecanismos de sincronización de nivel superior, como el lock .

Actualizar:

Una forma en que me gusta visualizar las cosas es asumir que el compilador de C #, JITer, etc. se optimizará de la manera más agresiva posible. Eso significa que Person.ctor podría ser un candidato para la inclusión (ya que es simple) lo que produciría el siguiente pseudocódigo.

Person ref = allocate space for Person ref.Name = name; ref.Age = age; person = instance; DoSomething(person);

Y como las escrituras no tienen una semántica de cerco de liberación en la especificación ECMA, las otras lecturas y escrituras podrían "flotar" más allá de la asignación a la person produce la siguiente secuencia válida de instrucciones.

Person ref = allocate space for Person person = ref; person.Name = name; person.Age = age; DoSomething(person);

Entonces, en este caso, puede ver que la person es asignada antes de que se inicialice. Esto es válido porque desde la perspectiva del subproceso en ejecución, la secuencia lógica permanece consistente con la secuencia física. No hay efectos secundarios no deseados. Pero, por razones que deberían ser obvias, esta secuencia sería desastrosa para otro hilo.


Bueno, al menos en el nivel de IL, se llama al constructor directamente en la pila, con la referencia resultante no generada (y se puede almacenar) hasta después de que se complete la construcción. Como tal, no se puede reordenar en el nivel del compilador (IL) (para tipos de referencia).

En cuanto al nivel de jitter, no estoy seguro, aunque me sorprendería si reordenara una asignación de campo y una llamada de método (que es lo que es un constructor). ¿Vería realmente el compilador el método y todas sus posibles vías de ejecución? ¿Para asegurarse de que el campo nunca sea utilizado por el método llamado?

Del mismo modo, a nivel de la CPU, me sorprendería si se produjera una reordenación alrededor de una instrucción de salto, ya que la CPU no tiene forma de saber si una rama es una "llamada de subrutina" y, por lo tanto, volverá a la siguiente instrucción. La ejecución fuera de orden permitiría un comportamiento incorrectamente grave en el caso de saltos "no convencionales".


No tienes esperanza. Reemplace la escritura de la consola con una comprobación de errores, configure una docena de copias de Thread1 (), use una máquina con 4 núcleos, y está obligado a encontrar algunas instancias de Person parcialmente construidas. Use las técnicas garantizadas mencionadas en otras respuestas y comentarios para mantener su programa seguro.

Tanto las personas que escriben compiladores como las personas que crean CPU''s, en su búsqueda de más velocidad, conspiran para empeorar la situación. Sin instrucciones explícitas para hacer lo contrario, la gente del compilador reordenará su código de cualquier manera posible para guardar un nanosegundo. La gente de la CPU está haciendo lo mismo. La última vez que leí, un solo núcleo tiende a ejecutar 4 instrucciones simultáneamente, si puede. (Y tal vez incluso si no puede).

En circunstancias normales, rara vez tendrá un problema con esto. Sin embargo, he descubierto que un problema menor que solo aparece una vez cada 6 meses puede ser un problema verdaderamente importante. Y, curiosamente, un problema de una vez en mil millones puede suceder varias veces por minuto, lo que es mucho más preferible. Supongo que su código caerá en la última categoría.