c# - seccion - ¿Hay una condición de carrera en este patrón común que se usa para prevenir NullReferenceException?
race condition vulnerability (2)
En CLR a través de C # (pp. 264–265), Jeffrey Richter analiza este problema específico y reconoce que es posible que la variable local se intercambie:
[T] su código podría ser optimizado por el compilador para eliminar la variable local […] por completo. Si esto sucede, esta versión del código es idéntica a la [versión que hace referencia al evento / devolución de llamada directamente dos veces], por lo que aún es posible una
NullReferenceException
.
Richter sugiere el uso de Interlocked.CompareExchange<T>
para resolver definitivamente este problema:
public void DoCallback()
{
Action local = Interlocked.CompareExchange(ref _Callback, null, null);
if (local != null)
local();
}
Sin embargo, Richter reconoce que el compilador Just-in-time (JIT) de Microsoft no optimiza la variable local; y, aunque esto podría, en teoría, cambiar, es casi seguro que nunca lo hará porque causaría que muchas aplicaciones se rompan como resultado.
Esta pregunta ya se ha formulado y se ha respondido en detalle en “ Optimización permitida del compilador de C # en variables locales y recuperación del valor de la memoria ”. Asegúrese de leer la respuesta de xanatox y el artículo " Entender el impacto de las técnicas de bloqueo bajo en aplicaciones de multiproceso " que cita. Dado que ha preguntado específicamente sobre Mono, debe prestar atención al modelo de memoria " [Mono-dev] referenciado" ? Mensaje de la lista de correo:
En este momento, proporcionamos una semántica flexible cercana a ecma respaldada por la arquitectura que está ejecutando.
Hice this pregunta y obtuve this respuesta interesante (y un poco desconcertante).
Daniel declara en su respuesta (a menos que lo esté leyendo incorrectamente) que la especificación CLI de ECMA-335 podría permitir que un compilador genere un código que genere una NullReferenceException
desde el siguiente método DoCallback
.
class MyClass {
private Action _Callback;
public Action Callback {
get { return _Callback; }
set { _Callback = value; }
}
public void DoCallback() {
Action local;
local = Callback;
if (local == null)
local = new Action(() => { });
local();
}
}
Él dice que, para garantizar que no se lance una NullReferenceException
, la palabra clave volatile
debe usar en _Callback
o se debe usar un lock
alrededor de la línea local = Callback;
.
¿Alguien puede corroborar eso? Y, si es cierto, ¿existe una diferencia en el comportamiento entre los compiladores Mono y .NET con respecto a este problema?
Editar
Aquí hay un enlace a la standard .
Actualizar
Creo que esta es la parte pertinente de la especificación (12.6.4):
Las implementaciones conformes de la CLI son libres de ejecutar programas utilizando cualquier tecnología que garantice, dentro de un solo hilo de ejecución, que los efectos secundarios y las excepciones generadas por un hilo sean visibles en el orden especificado por el CIL. Para este propósito, solo las operaciones volátiles (incluidas las lecturas volátiles) constituyen efectos secundarios visibles. (Tenga en cuenta que aunque solo las operaciones volátiles constituyen efectos secundarios visibles, las operaciones volátiles también afectan la visibilidad de las referencias no volátiles). Las operaciones volátiles se especifican en §12.6.7. No hay garantías de pedido relativas a las excepciones inyectadas en un subproceso por otro subproceso (tales excepciones a veces se denominan "excepciones asíncronas" (por ejemplo, System.Threading.ThreadAbortException).
[Fundamento: un compilador optimizador es libre de reordenar los efectos secundarios y las excepciones sincrónicas en la medida en que esta reordenación no cambie ningún comportamiento observable del programa. justificación final]
[Nota: se permite una implementación de la CLI para usar un compilador de optimización, por ejemplo, para convertir CIL a código de máquina nativo siempre que el compilador mantenga (dentro de cada subproceso de ejecución) el mismo orden de efectos secundarios y excepciones síncronas.
Entonces ... tengo curiosidad por saber si esta declaración permite o no que un compilador optimice la propiedad de Callback
(que accede a un campo simple) y la variable local
para producir lo siguiente, que tiene el mismo comportamiento en un solo hilo de ejecución :
if (_Callback != null) _Callback();
else new Action(() => { })();
La sección 12.6.7 de la palabra clave volatile
parece ofrecer una solución para los programadores que desean evitar la optimización:
Una lectura volátil tiene "adquisición de semántica", lo que significa que se garantiza que la lectura se producirá antes de cualquier referencia a la memoria que ocurra después de la instrucción de lectura en la secuencia de instrucciones CIL. Una escritura volátil tiene una "semántica de liberación", lo que significa que se garantiza que la escritura suceda después de cualquier referencia de memoria antes de la instrucción de escritura en la secuencia de instrucciones CIL. Una implementación conforme de la CLI garantizará esta semántica de operaciones volátiles. Esto garantiza que todos los subprocesos observarán escrituras volátiles realizadas por cualquier otro subproceso en el orden en que se realizaron. Pero no se requiere una implementación conforme para proporcionar un solo ordenamiento total de escrituras volátiles como se ve en todos los subprocesos de ejecución. Un compilador de optimización que convierte CIL a código nativo no eliminará ninguna operación volátil, ni unirá múltiples operaciones volátiles en una sola operación.
Este código no lanzará una excepción de referencia nula. Este es seguro para subprocesos:
public void DoCallback() {
Action local;
local = Callback;
if (local == null)
local = new Action(() => { });
local();
}
La razón por la que este es seguro para subprocesos, y no puede lanzar una NullReferenceException en la devolución de llamada, es que se está copiando en una variable local antes de realizar la comprobación / llamada nula. Incluso si la devolución de llamada original se estableció en nula después de la comprobación nula, la variable local seguirá siendo válida.
Sin embargo la siguiente es una historia diferente:
public void DoCallbackIfElse() {
if (null != Callback) Callback();
else new Action(() => { })();
}
En este caso se trata de una variable pública, la devolución de llamada se puede cambiar a nula DESPUÉS de if (null != Callback)
que generaría una excepción en la Callback();
de Callback();