Hay una condición de carrera en este patrón común que se utiliza para prevenir NullReferenceException?


Pregunté este pregunta y tengo este interesante (y un poco desconcertante) respuesta.

Daniel afirma en su respuesta (a menos que lo esté leyendo incorrectamente) que la especificación ECMA-335 CLI podría permitir que un compilador genere código que arroje un NullReferenceException del 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();
    }
}

Dice que, para garantizar que no se arroje un NullReferenceException, la palabra clave volatile debe usarse en _Callback o debe usarse un lock se utilizará alrededor de la línea local = Callback;.

¿Puede alguien corroborar eso? Y, si es cierto, ¿hay una diferencia en el comportamiento entre los compiladores Mono y . NET con respecto a este problema?

Editar
Aquí hay un enlace a la estándar.

Update
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 usando cualquiera tecnología que garantiza, dentro de un solo hilo de ejecución, que los efectos secundarios y las excepciones generadas por un hilo son visible en el orden especificado por el CIL. Para este propósito solamente las operaciones volátiles (incluidas las lecturas volátiles) constituyen efectos secundarios. (Tenga en cuenta que mientras que sólo las operaciones volátiles constituyen los efectos secundarios visibles, las operaciones volátiles también afectan la visibilidad de referencias no volátiles.) Las operaciones volátiles se especifican en §12.6.7. No hay ordenar garantías relativas a excepciones inyectado en un hilo por otro hilo (tales excepciones son a veces llamadas "excepciones asíncronas" (p. ej., Sistema.Enhebrando.ThreadAbortException).

[Justificación: Una optimización compilador es libre de reordenar los efectos secundarios y las excepciones síncronas a la medida en que este reordenamiento no cambia ningún programa observable comportamiento. end rationale]

[Nota: Una implementación de la CLI es permitido utilizar un optimizando el compilador, por ejemplo, para convertir CIL código de máquina nativo siempre que el compilador mantenga (dentro de cada un único hilo de ejecución) el mismo orden de efectos secundarios y excepciones sincrónicas.

So... Tengo curiosidad en cuanto a si esta instrucción permite o no que un compilador optimice la propiedad 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 sobre la palabra clave volatile parece ofrecer una solución para los programadores que deseen evitar la optimización:

Una lectura volátil tiene "adquirir semántica", lo que significa que la lectura es garantizado que ocurra antes de cualquier referencia a la memoria que ocurra después la instrucción de lectura en la secuencia de instrucción CIL. Una escritura volátil tiene "semántica de liberación", lo que significa que la escritura está garantizada después de cualquier referencia de memoria anterior a la instrucción de escritura en el CIL secuencia de instrucciones. Una implementación conforme de la CLI deberá garantizar esta semántica de las operaciones volátiles. Esto asegura que todos los hilos observarán escrituras volátiles realizadas por cualquier otro hilo en el orden en que se realizaron. Pero una implementación conforme no es requerido para proporcionar un único orden total de escrituras volátiles como se ve de todos los hilos de ejecución. Un compilador de optimización que convierte CIL al código nativo no eliminar cualquier operación volátil, ni reúne múltiples operaciones volátiles en una sola operación.

Author: Community, 2012-05-14

2 answers

En CLR vía C# (pp. 264-265), Jeffrey Richter discute este problema específico, y reconoce que es posible que la variable local sea intercambiada:

[E] su código podría ser optimizado por el compilador para eliminar completamente la variable local [.]. Si esto sucede, esta versión del código es idéntica a la [versión que hace referencia al evento/callback directamente dos veces], por lo que un NullReferenceException todavía es posible.

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 hace no optimizar la variable local; y, aunque esto podría, en teoría, cambiar, es casi seguro que nunca lo hará porque causaría demasiadas aplicaciones para romper como resultado.

Esta pregunta ya se ha hecho y respondido ampliamente en " Permite la optimización del compilador de C # en variables locales y el valor de refetching from memory ". Asegúrese de leer la respuesta de xanatox y el artículo " Comprender el Impacto de las Técnicas de Bloqueo Bajo en Aplicaciones Multiproceso" que cita. Dado que preguntó específicamente sobre Mono, debe prestar atención a la referencia " [Mono-dev] Modelo de memoria? " mensaje de la lista de correo:

Ahora mismo proporcionamos semántica suelta cercana a ecma respaldada por la arquitectura que está ejecutando.

 11
Author: Douglas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:28:14

Este código no lanzar una excepción de referencia nula. Este es un hilo seguro:

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 excepción NullReferenceException en la devolución de llamada, es que está copiando a una variable local antes de hacer su comprobación / llamada null. Incluso si la devolución de llamada original se estableció en null después de la comprobación de null, la variable local seguirá siendo válida.

Sin embargo, lo siguiente es una historia diferente:

public void DoCallbackIfElse() {
    if (null != Callback) Callback();
    else new Action(() => { })();
}

En este se trata de mirar a un público variable, la devolución de llamada se puede cambiar a null DESPUÉS de if (null != Callback) que lanzaría una excepción en Callback();

 3
Author: caesay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-14 19:51:58