sql >> Base de Datos >  >> RDS >> Database

Eventos y subprocesos en .NET

Me gustaría decirle directamente que este artículo no se referirá a subprocesos en particular, sino a eventos en el contexto de subprocesos en .NET. Por lo tanto, no intentaré organizar los hilos correctamente (con todos los bloqueos, devoluciones de llamadas, cancelaciones, etc.). Hay muchos artículos sobre este tema.

Todos los ejemplos están escritos en C# para la versión 4.0 del framework (en 4.6, todo es un poco más fácil, pero aun así, hay muchos proyectos en 4.0). También intentaré ceñirme a la versión 5.0 de C#.

En primer lugar, me gustaría señalar que hay delegados listos para el sistema de eventos .Net que recomiendo usar en lugar de inventar algo nuevo. Por ejemplo, con frecuencia me enfrenté a los siguientes 2 métodos para organizar eventos.

Primer método:

 class WrongRaiser
    {
        public event Action<object> MyEvent;
        public event Action MyEvent2;
    }

Recomendaría usar este método con cuidado. Si no lo universaliza, eventualmente puede escribir más código del esperado. Como tal, no establecerá una estructura más precisa si se compara con los métodos a continuación.

Por mi experiencia, puedo decir que lo usé cuando comencé a trabajar con eventos y, en consecuencia, hice el ridículo. Ahora, nunca haría que sucediera.

Segundo método:

    class WrongRaiser
    {
        public event MyDelegate MyEvent;
    }

    class MyEventArgs
    {
        public object SomeProperty { get; set; }
    }

    delegate void MyDelegate(object sender, MyEventArgs e);

Este método es bastante válido, pero es bueno para casos específicos cuando el método a continuación no funciona por alguna razón. De lo contrario, es posible que obtenga mucho trabajo monótono.

Y ahora, echemos un vistazo a lo que ya se ha creado para los eventos.

Método universal:

    class Raiser
    {
        public event EventHandler<MyEventArgs> MyEvent;
    }

    class MyEventArgs : EventArgs
    {
        public object SomeProperty { get; set; }
    }

Como puede ver, aquí usamos la clase universal EventHandler. Es decir, no hay necesidad de definir su propio controlador.

Los ejemplos adicionales presentan el método universal.

Echemos un vistazo al ejemplo más simple del generador de eventos.

    class EventRaiser
    {
        int _counter;

        public event EventHandler<EventRaiserCounterChangedEventArgs> CounterChanged;

        public int Counter
        {
            get
            {
                return _counter;
            }

            set
            {
                if (_counter != value)
                {
                    var old = _counter;
                    _counter = value;
                    OnCounterChanged(old, value);
                }
            }
        }

        public void DoWork()
        {
            new Thread(new ThreadStart(() =>
            {
                for (var i = 0; i < 10; i++)
                    Counter = i;
            })).Start();
        }

        void OnCounterChanged(int oldValue, int newValue)
        {
            if (CounterChanged != null)
                CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue));
        }
    }

    class EventRaiserCounterChangedEventArgs : EventArgs
    {
        public int NewValue { get; set; }
        public int OldValue { get; set; }
        public EventRaiserCounterChangedEventArgs(int oldValue, int newValue)
        {
            NewValue = newValue;
            OldValue = oldValue;
        }
    }

Aquí tenemos una clase con la propiedad Contador que se puede cambiar de 0 a 10. En ese momento, la lógica que cambia el Contador se procesa en un subproceso separado.

Y aquí está nuestro punto de entrada:

    class Program
    {
        static void Main(string[] args)
        {
            var raiser = new EventRaiser();
            raiser.CounterChanged += Raiser_CounterChanged;
            raiser.DoWork();
            Console.ReadLine();
        }

        static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
        {
            Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
        }
    }

Es decir, creamos una instancia de nuestro generador, nos suscribimos al cambio de contador y, en el controlador de eventos, enviamos valores a la consola.

Esto es lo que obtenemos como resultado:

Hasta aquí todo bien. Pero pensemos, ¿en qué subproceso se ejecuta el controlador de eventos?

La mayoría de mis colegas respondieron a esta pregunta "En general". Significaba que ninguno de ellos no entendía cómo se organizan los delegados. Intentaré explicarlo.

La clase Delegado contiene información sobre un método.

También está su descendiente, MulticastDelegate, que tiene más de un elemento.

Entonces, cuando se suscribe a un evento, se crea una instancia del descendiente de MulticastDelegate. Cada siguiente suscriptor agrega un nuevo método (controlador de eventos) en la instancia ya creada de MulticastDelegate.

Cuando llama al método Invoke, los controladores de todos los suscriptores se llaman uno por uno para su evento. En eso, el subproceso en el que llama a estos controladores no sabe nada sobre el subproceso en el que se especificaron y, en consecuencia, no puede insertar nada en ese subproceso.

En general, los controladores de eventos del ejemplo anterior se ejecutan en el hilo generado en el método DoWork(). Es decir, durante la generación del evento, el hilo que lo generó de esa manera está esperando la ejecución de todos los controladores. Te mostraré esto sin retirar hilos de identificación. Para esto, cambié algunas líneas de código en el ejemplo anterior.

Prueba de que todos los controladores del ejemplo anterior se ejecutan en el subproceso que llamó al evento

Método donde se genera el evento

        void OnCounterChanged(int oldValue, int newValue)
        {
            if (CounterChanged != null)
            {
                CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue));
                Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue));
            }
                
        }

Manejador

        static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
        {
            Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
            Thread.Sleep(500);
        }

En el controlador, enviamos el subproceso actual a dormir durante medio segundo. Si los controladores funcionaran en el subproceso principal, esta vez sería suficiente para que un subproceso generado en DoWork() termine su trabajo y genere sus resultados.

Sin embargo, esto es lo que realmente vemos:

No sé quién y cómo debería manejar los eventos generados por la clase que escribí, pero realmente no quiero que estos controladores retrasen el trabajo de mi clase. Por eso, usaré el método BeginInvoke en lugar de Invoke. BeginInvoke genera un nuevo hilo.

Nota:Ambos métodos, Invoke y BeginInvoke, no son miembros de las clases Delegate o MulticastDelegate. Son los miembros de la clase generada (o la clase universal descrita anteriormente).

Ahora bien, si cambiamos el método en el que se genera el evento, obtendremos lo siguiente:

Generación de eventos de subprocesos múltiples:

        void OnCounterChanged(int oldValue, int newValue)
        {
            if (CounterChanged != null)
            {
                var delegates = CounterChanged.GetInvocationList();
                for (var i = 0; i < delegates.Length; i++)
                    ((EventHandler<EventRaiserCounterChangedEventArgs>)delegates[i]).BeginInvoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue), null, null);
                Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue));
            }
                
        }

Los dos últimos parámetros son iguales a nulo. El primero es una devolución de llamada, el segundo es un parámetro determinado. No uso devolución de llamada en este ejemplo, ya que el ejemplo es intermedio. Puede ser útil para la retroalimentación. Por ejemplo, puede ayudar a la clase que genera el evento a determinar si un evento fue manejado y/o si se requiere para obtener resultados de este manejo. También puede liberar recursos relacionados con la operación asíncrona.

Si ejecutamos el programa, obtendremos el siguiente resultado.

Supongo que está bastante claro que ahora los controladores de eventos se ejecutan en subprocesos separados, es decir, al generador de eventos no le importa quién, cómo y por cuánto tiempo manejará sus eventos.

Y aquí surge la pregunta:¿qué pasa con el manejo secuencial? Tenemos Counter, después de todo. ¿Y si fuera un cambio de estado en serie? Pero no responderé a esta pregunta, no es un tema de este artículo. Solo puedo decir que hay varias formas.

Y una cosa más. Para no repetir las mismas acciones una y otra vez, sugiero crear una clase separada para ellas.

Una clase para la generación de eventos asíncronos

    static class AsyncEventsHelper
    {
        public static void RaiseEventAsync<T>(EventHandler<T> h, object sender, T e) where T : EventArgs
        {
            if (h != null)
            {
                var delegates = h.GetInvocationList();
                for (var i = 0; i < delegates.Length; i++)
                    ((EventHandler<T>)delegates[i]).BeginInvoke(sender, e, h.EndInvoke, null);
            }
        }
    }

En este caso, usamos la devolución de llamada. Se ejecuta en el mismo hilo que el controlador. Es decir, una vez que se completa el método del controlador, el delegado llama a h.EndInvoke a continuación.

Así es como se debe usar

        void OnCounterChanged(int oldValue, int newValue)
        {
            AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); 
        }

Supongo que ahora está claro por qué se necesitaba el método universal. Si describimos eventos con el método 2, este truco no funcionará. De lo contrario, tendrá que crear la universalidad para sus delegados por su cuenta.

Nota :Para proyectos reales, recomiendo cambiar la arquitectura de eventos en el contexto de los hilos. Los ejemplos descritos pueden dañar el trabajo de la aplicación con subprocesos y se proporcionan únicamente con fines informativos.

Conclusión

Espero, logré describir cómo funcionan los eventos y dónde funcionan los controladores. En el próximo artículo, planeo profundizar en la obtención de resultados del manejo de eventos cuando se realiza una llamada asincrónica.

Espero sus comentarios y sugerencias.