La asignación de referencia es atómica, entonces, ¿por qué se necesita Interlocked.Exchange (ref Object, Object)?

108

En mi servicio web asmx multiproceso, tenía un campo de clase _allData de mi propio tipo SystemData que consta de pocos List<T>y está Dictionary<T>marcado como volatile. Los datos del sistema ( _allData) se actualizan de vez en cuando y lo hago creando otro objeto llamado newDatay lleno sus estructuras de datos con nuevos datos. Cuando termine, solo asigno

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Esto debería funcionar ya que la asignación es atómica y los subprocesos que tienen la referencia a datos antiguos siguen usándola y el resto tiene los nuevos datos del sistema justo después de la asignación. Sin embargo, mi colega dijo que en lugar de usar una volatilepalabra clave y una asignación simple, debería usar InterLocked.Exchangeporque dijo que en algunas plataformas no se garantiza que la asignación de referencia sea atómica. Además: cuando declaro the _allDatacampo como volatileel

Interlocked.Exchange<SystemData>(ref _allData, newData); 

produce una advertencia "una referencia a un campo volátil no se tratará como volátil" ¿Qué debo pensar al respecto?

encanto
fuente

Respuestas:

180

Aquí hay numerosas preguntas. Considerándolos uno a la vez:

La asignación de referencia es atómica, entonces, ¿por qué se necesita Interlocked.Exchange (ref Object, Object)?

La asignación de referencia es atómica. Interlocked.Exchange no solo hace referencia a la asignación. Hace una lectura del valor actual de una variable, oculta el valor anterior y asigna el nuevo valor a la variable, todo como una operación atómica.

mi colega dijo que en algunas plataformas no se garantiza que la asignación de referencias sea atómica. ¿Tenía razón mi colega?

No. Se garantiza que la asignación de referencia será atómica en todas las plataformas .NET.

Mi colega está razonando desde premisas falsas. ¿Significa eso que sus conclusiones son incorrectas?

No necesariamente. Su colega podría estar dándole buenos consejos por malas razones. Quizás haya alguna otra razón por la que debería utilizar Interlocked.Exchange. La programación sin candados es increíblemente difícil y en el momento en que se aparta de las prácticas bien establecidas defendidas por los expertos en el campo, se pierde en la maleza y se arriesga a las peores condiciones de carrera. No soy un experto en este campo ni un experto en su código, por lo que no puedo emitir un juicio de una forma u otra.

produce una advertencia "una referencia a un campo volátil no se tratará como volátil" ¿Qué debo pensar al respecto?

Debe comprender por qué esto es un problema en general. Eso conducirá a comprender por qué la advertencia no es importante en este caso particular.

La razón por la que el compilador da esta advertencia es porque marcar un campo como volátil significa "este campo se actualizará en varios subprocesos; no genere ningún código que almacene en caché los valores de este campo y asegúrese de que las lecturas o escrituras de este campo no se "mueve hacia adelante y hacia atrás en el tiempo" a través de inconsistencias de caché del procesador ".

(Supongo que ya comprende todo eso. Si no tiene una comprensión detallada del significado de volátil y cómo afecta la semántica de la caché del procesador, entonces no comprende cómo funciona y no debería usar volátil. Programas sin bloqueo son muy difíciles de hacer bien; asegúrese de que su programa sea correcto porque comprende cómo funciona, no por accidente).

Ahora suponga que crea una variable que es un alias de un campo volátil pasando una referencia a ese campo. Dentro del método llamado, el compilador no tiene ningún motivo para saber que la referencia debe tener semántica volátil. El compilador generará alegremente código para el método que no implementa las reglas para los campos volátiles, pero la variable es un campo volátil. Eso puede arruinar completamente su lógica libre de bloqueos; siempre se asume que siempre se accede a un campo volátil con semántica volátil. No tiene sentido tratarlo como volátil algunas veces y otras no; tiene que ser siempre coherente, de lo contrario no puede garantizar la coherencia en otros accesos.

Por lo tanto, el compilador le advierte cuando hace esto, porque probablemente va a estropear por completo su lógica sin bloqueos cuidadosamente desarrollada.

Por supuesto, Interlocked.Exchange está escrito para esperar un campo volátil y hacer lo correcto. Por tanto, la advertencia es engañosa. Lo lamento mucho; lo que deberíamos haber hecho es implementar algún mecanismo mediante el cual un autor de un método como Interlocked.Exchange podría poner un atributo en el método diciendo "este método que toma una referencia aplica la semántica volátil en la variable, así que suprime la advertencia". Quizás en una versión futura del compilador lo hagamos.

Eric Lippert
fuente
1
Por lo que he oído, Interlocked.Exchange también garantiza que se crea una barrera de memoria. Entonces, si, por ejemplo, crea un nuevo objeto, luego asigna un par de propiedades y luego almacena el objeto en otra referencia sin usar Interlocked.Exchange, entonces el compilador puede alterar el orden de esas operaciones, haciendo que el acceso a la segunda referencia no sea un hilo. seguro. ¿Es eso realmente así? ¿Tiene sentido usar Interlocked.Exchange es ese tipo de escenarios?
Mike
12
@Mike: Cuando se trata de lo que posiblemente se observa en situaciones de multiproceso de bloqueo bajo, soy tan ignorante como cualquier otro. La respuesta probablemente variará de un procesador a otro. Debe dirigir su pregunta a un experto o leer sobre el tema si le interesa. El libro y el blog de Joe Duffy son buenos lugares para comenzar. Mi regla: no use subprocesos múltiples. Si es necesario, utilice estructuras de datos inmutables. Si no puede, use candados. Solo cuando deba tener datos mutables sin bloqueos, debe considerar las técnicas de bloqueo bajo.
Eric Lippert
Gracias por tu respuesta Eric. De hecho, me interesa, es por eso que he estado leyendo libros y blogs sobre estrategias de bloqueo y subprocesos múltiples y también tratando de implementarlos en mi código. Pero todavía hay mucho que aprender ...
Mike
2
@EricLippert Entre "no usar subprocesos múltiples" y "si es necesario, usar estructuras de datos inmutables", insertaría el nivel intermedio y muy común de "hacer que un subproceso secundario use solo objetos de entrada de propiedad exclusiva y el subproceso principal consuma los resultados sólo cuando el niño haya terminado ". Como en var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
Juan
1
@John: Esa es una buena idea. Intento tratar los hilos como procesos baratos: están ahí para hacer un trabajo y producir un resultado, no para correr como un segundo hilo de control dentro de las estructuras de datos del programa principal. Pero si la cantidad de trabajo que está haciendo el hilo es tan grande que es razonable tratarlo como un proceso, entonces digo ¡simplemente conviértalo en un proceso!
Eric Lippert
9

O su colega se equivoca o sabe algo que la especificación del lenguaje C # no sabe.

5.5 Atomicidad de referencias variables :

"Las lecturas y escrituras de los siguientes tipos de datos son atómicas: bool, char, byte, sbyte, short, ushort, uint, int, float y tipos de referencia".

Por lo tanto, puede escribir en la referencia volátil sin riesgo de obtener un valor dañado.

Por supuesto, debe tener cuidado con la forma en que decide qué hilo debe buscar los nuevos datos, para minimizar el riesgo de que más de un hilo a la vez lo haga.

Guffa
fuente
3
@guffa: sí, también lo he leído. esto deja la pregunta original "la asignación de referencia es atómica entonces ¿por qué se necesita Interlocked.Exchange (ref Object, Object)?" sin respuesta
char m
@zebrabox: ¿qué quieres decir? cuando no lo son? ¿qué harías?
char m
@matti: es necesario cuando tienes que leer y escribir un valor como una operación atómica.
Guffa
¿Con qué frecuencia tiene que preocuparse de que la memoria no esté alineada correctamente en .NET realmente? ¿Cosas de interoperabilidad?
Skurmedel
1
@zebrabox: La especificación no incluye esa advertencia, da una declaración muy clara. ¿Tiene una referencia para una situación no alineada con la memoria en la que una lectura o escritura de referencia no es atómica? Parece que eso violaría el lenguaje muy claro de la especificación.
TJ Crowder
6

Interbloqueado Intercambio <T>

Establece una variable del tipo T especificado en un valor especificado y devuelve el valor original, como una operación atómica.

Cambia y devuelve el valor original, es inútil porque solo quieres cambiarlo y, como dijo Guffa, ya es atómico.

A menos que un generador de perfiles haya demostrado que es un cuello de botella en su aplicación, debe considerar deshacer los bloqueos, es más fácil de entender y demostrar que su código es correcto.

Guillaume
fuente
3

Iterlocked.Exchange() no es solo atómico, también se ocupa de la visibilidad de la memoria:

Las siguientes funciones de sincronización utilizan las barreras adecuadas para garantizar el orden de la memoria:

Funciones que entran o salen de secciones críticas

Funciones que señalan objetos de sincronización

Funciones de espera

Funciones enclavadas

Problemas de sincronización y multiprocesador

Esto significa que además de la atomicidad asegura que:

  • Para el hilo que lo llama:
    • No se realiza ningún reordenamiento de las instrucciones (por parte del compilador, el tiempo de ejecución o el hardware).
  • Para todos los hilos:
    • Ninguna lectura en la memoria que ocurra antes de esta instrucción verá el cambio que esta instrucción realizó.
    • Todas las lecturas posteriores a esta instrucción verán el cambio realizado por esta instrucción.
    • Todas las escrituras en la memoria después de esta instrucción sucederán después de que este cambio de instrucción haya llegado a la memoria principal (al vaciar este cambio de instrucción a la memoria principal cuando esté hecho y no dejar que el hardware lo limpie por sí mismo).
Sellador
fuente