Database
 sql >> Database >  >> RDS >> Database

Eventi e thread in .NET

Vorrei dirti subito che questo articolo non riguarderà i thread in particolare, ma gli eventi nel contesto dei thread in .NET. Quindi, non proverò a organizzare correttamente i thread (con tutti i blocchi, i callback, l'annullamento, ecc.). Ci sono molti articoli su questo argomento.

Tutti gli esempi sono scritti in C# per il framework versione 4.0 (in 4.6 tutto è leggermente più semplice, ma ci sono ancora molti progetti in 4.0). Cercherò anche di attenermi alla versione C# 5.0.

In primo luogo, vorrei notare che ci sono delegati pronti per il sistema di eventi .Net che consiglio vivamente di utilizzare invece di inventare qualcosa di nuovo. Ad esempio, ho spesso affrontato i seguenti 2 metodi per organizzare eventi.

Primo metodo:

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

Consiglierei di usare questo metodo con attenzione. Se non lo universalizzi, potresti eventualmente scrivere più codice del previsto. In quanto tale, non imposterà una struttura più precisa rispetto ai metodi seguenti.

Dalla mia esperienza posso dire che l'ho usato quando ho iniziato a lavorare con gli eventi e di conseguenza mi sono reso ridicolo. Ora, non lo farei mai accadere.

Secondo metodo:

    class WrongRaiser
    {
        public event MyDelegate MyEvent;
    }

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

    delegate void MyDelegate(object sender, MyEventArgs e);

Questo metodo è abbastanza valido, ma è utile per casi specifici in cui il metodo seguente non funziona per alcuni motivi. In caso contrario, potresti ottenere un sacco di lavoro monotono.

E ora, diamo un'occhiata a ciò che è già stato creato per gli eventi.

Metodo universale:

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

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

Come puoi vedere, qui utilizziamo la classe EventHandler universale. Cioè, non è necessario definire il proprio gestore.

Gli ulteriori esempi presentano il metodo universale.

Diamo un'occhiata all'esempio più semplice del generatore di eventi.

    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;
        }
    }

Qui abbiamo una classe con la proprietà Counter che può essere modificata da 0 a 10. A questo punto, la logica che cambia Counter viene elaborata in un thread separato.

Ed ecco il nostro punto di ingresso:

    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));
        }
    }

Cioè, creiamo un'istanza del nostro generatore, sottoscriviamo la modifica del contatore e, nel gestore dell'evento, emettiamo i valori alla console.

Ecco cosa otteniamo come risultato:

Fin qui tutto bene. Ma pensiamo, in quale thread viene eseguito il gestore di eventi?

La maggior parte dei miei colleghi ha risposto a questa domanda "In generale". Significava che nessuno di loro non capiva come sono organizzati i delegati. Proverò a spiegarlo.

La classe Delegate contiene informazioni su un metodo.

C'è anche il suo discendente, MulticastDelegate, che ha più di un elemento.

Pertanto, quando ti iscrivi a un evento, viene creata un'istanza del discendente MulticastDelegate. Ogni abbonato successivo aggiunge un nuovo metodo (gestore di eventi) nell'istanza già creata di MulticastDelegate.

Quando chiami il metodo Invoke, i gestori di tutti i sottoscrittori vengono chiamati uno per uno per il tuo evento. In questo caso, il thread in cui chiami questi gestori non sa nulla del thread in cui sono stati specificati e, di conseguenza, non può inserire nulla in quel thread.

In generale, i gestori di eventi nell'esempio precedente vengono eseguiti nel thread generato nel metodo DoWork(). Cioè, durante la generazione dell'evento, il thread che lo ha generato in questo modo è in attesa dell'esecuzione di tutti i gestori. Ti mostrerò questo senza ritirare i thread di ID. Per questo, ho cambiato alcune righe di codice nell'esempio sopra.

Dimostra che tutti i gestori nell'esempio precedente vengono eseguiti nel thread che ha chiamato l'evento

Metodo di generazione dell'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));
            }
                
        }

gestore

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

Nel gestore, mandiamo il thread corrente a dormire per mezzo secondo. Se i gestori funzionassero nel thread principale, questa volta sarebbe sufficiente perché un thread generato in DoWork() finisca il suo lavoro e produca i suoi risultati.

Tuttavia, ecco cosa vediamo davvero:

Non so chi e come debba gestire gli eventi generati dalla classe che ho scritto, ma non voglio davvero che questi gestori rallentino il lavoro della mia classe. Ecco perché userò il metodo BeginInvoke invece di Invoke. BeginInvoke genera un nuovo thread.

Nota:entrambi i metodi Invoke e BeginInvoke non sono membri delle classi Delegate o MulticastDelegate. Sono i membri della classe generata (o della classe universale sopra descritta).

Ora, se cambiamo il metodo con cui viene generato l'evento, otterremo quanto segue:

Generazione di eventi multi-thread:

        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));
            }
                
        }

Gli ultimi due parametri sono nulli. Il primo è una richiamata, il secondo è un determinato parametro. Non utilizzo il callback in questo esempio, poiché l'esempio è intermedio. Può essere utile per un feedback. Ad esempio, può aiutare la classe che genera l'evento a determinare se un evento è stato gestito e/o se è necessario ottenere i risultati di questa gestione. Può anche liberare risorse relative al funzionamento asincrono.

Se eseguiamo il programma, otterremo il seguente risultato.

Immagino sia abbastanza chiaro che ora i gestori di eventi vengono eseguiti in thread separati, ovvero il generatore di eventi non si preoccupa di chi, come e per quanto tempo gestirà i suoi eventi.

E qui sorge la domanda:che dire della gestione sequenziale? Abbiamo Counter, dopotutto. E se fosse un cambio seriale di stati? Ma non risponderò a questa domanda, non è un argomento di questo articolo. Posso solo dire che ci sono diversi modi.

E un'altra cosa. Per non ripetere le stesse azioni più e più volte, suggerisco di creare una classe separata per loro.

Una classe per la generazione di eventi asincroni

    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);
            }
        }
    }

In questo caso, utilizziamo la richiamata. Viene eseguito nello stesso thread del gestore. Ovvero, dopo che il metodo del gestore è stato completato, il delegato chiama h.EndInvoke next.

Ecco come dovrebbe essere usato

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

Immagino che ora sia chiaro il motivo per cui era necessario il metodo universale. Se descriviamo gli eventi con il metodo 2, questo trucco non funzionerà. Altrimenti, dovrai creare tu stesso l'universalità per i tuoi delegati.

Nota :Per i progetti reali, consiglio di modificare l'architettura degli eventi nel contesto dei thread. Gli esempi descritti possono danneggiare il lavoro di applicazione con i thread e sono forniti solo a scopo informativo.

Conclusione

Spero, sono riuscito a descrivere come funzionano gli eventi e dove funzionano i gestori. Nel prossimo articolo, ho intenzione di approfondire la ricerca dei risultati della gestione degli eventi quando viene effettuata una chiamata asincrona.

Attendo con impazienza i vostri commenti e suggerimenti.