Hoy encontré un problema extraño en nuestra aplicación .NET de subprocesos múltiples.
Estaba escribiendo un accesor de evento estático público así:
private static readonly object myLock = new object(); private static Action<MyType, Guid, Guid> _handler; public static event Action<MyType, Guid, Guid> MyEvent { add { lock (myLock) _handler += value; } remove { lock (myLock) _handler -= value; } }Esta pieza de código vive en una clase no estática .
El problema al que me enfrento es que una vez que uso el descriptor de acceso add (en un punto bastante temprano de la ejecución de la aplicación): MyClass.MyEvent += myMethod; , obtengo una excepción de referencia nula en la llamada al método de bloqueo en el descriptor de acceso agregar. A través de la depuración, descubrí que esto se debe a que, por alguna razón, el objeto de bloqueo estático privado es (todavía) nulo en el punto en que el código llega a la declaración de bloqueo.
Esto es desconcertante para mí. Tengo entendido que todos los miembros estáticos deben inicializarse en el punto del primer acceso a la clase. Más tarde me di cuenta de que el código parece funcionar si muevo el objeto de bloqueo a la parte superior de la clase, lo que significa que se inicializa en un punto mucho más temprano. Esto funciona, pero me gustaría mantener todo el código relevante en un solo lugar, así que no me gusta esta solución.
FYI, el archivo en el que estoy trabajando tiene casi 5000 líneas de código, y el fragmento anterior está cerca del final. Aunque no tengo idea si eso hace alguna diferencia...
Una teoría nuestra ha sido que el problema tiene que ver con que estamos trabajando con varios hilos. Más preciso que eso, no lo hemos averiguado. Se siente un poco tonto que esta sea la causa o el problema, ya que la razón por la que queremos usar un accesor de eventos estático con bloqueos es para manejar el acceso de subprocesos múltiples...
ACTUALIZAR
Entonces resulta que ahora de repente solo funciona. Como debería.
Sinceramente, no tengo idea de cuál fue el problema ni la solución. Simplemente continué con mi día e implementé algunos otros campos de eventos estáticos, además de suscribirme a estos desde otro archivo. Habiendo recompilado el proyecto, ahora funciona como debería. No edité el orden de las declaraciones en el archivo por cierto.
Gracias a todos por todas sus sugerencias, pensamientos e información. Me doy cuenta de que la forma de esta implementación (evento estático público) puede no ser óptima, pero fue la ruta elegida para esta implementación por el momento.
De los comentarios,
"¿Que los accesores generados por el compilador no son seguros para subprocesos?" - "eventos similares a campos" generados por el compilador (es decir,
public static event Action<MyType, Guid, Guid> MyEvent; están garantizados por la especificación para ser seguros para subprocesos (el compilador actualmente usa un intercambio entrelazado en bucle, pero el lock también se ha usado en el pasado).
Entonces: en realidad no necesitas nada aquí. Dicho esto, los eventos static suelen ser una mala idea y pueden provocar pérdidas de memoria. Pero: también tenemos un problema de inicialización de campos; ahora, como sugieres, el inicializador de campo estático aquí:
private static object myLock = new object(); absolutamente debe estar listo cuando se necesita; el tiempo de ejecución garantiza que se ejecuten los inicializadores estáticos, por lo que debería estar bien; podría intentar agregar readonly para asegurarse de que no tiene un código que acceda incorrectamente, pero debo estar de acuerdo: este código debería funcionar bien. Probablemente sería necesaria una reproducción completa (pero mínima) para investigarlo. La otra cosa (además de readonly ) que me interesaría buscar serían otros inicializadores de campo , que podrían hacer que este evento se toque, por ejemplo:
private static Foo foo = new Foo(); private static object myLock = new object(); Si el constructor Foo() vuelve a llamar al evento, por ejemplo:
class Foo { public Foo() { YourType.MyEvent += SomeHandler; } } entonces el inicializador de campo aún no habrá terminado y no habrá alcanzado la asignación de myLock . Se conserva el orden de los inicializadores de campos estáticos, teniendo en cuenta que si tiene varios archivos de partial class , ese orden en sí mismo no está definido. En ese escenario, asegurarse de que estén ordenados correctamente debería solucionarlo:
// IMPORTANT: make sure this stays first private static object myLock = new object(); private static Foo foo = new Foo();