Eu gostaria de lhe dizer imediatamente que este artigo não se referirá a threads em particular, mas a eventos no contexto de threads em .NET. Então, não vou tentar organizar as threads corretamente (com todos os blocos, callbacks, cancelamentos, etc.) Existem muitos artigos sobre esse assunto.
Todos os exemplos são escritos em C# para o framework versão 4.0 (em 4.6, tudo é um pouco mais fácil, mas ainda assim, existem muitos projetos em 4.0). Também tentarei manter o C# versão 5.0.
Em primeiro lugar, gostaria de observar que existem delegados prontos para o sistema de eventos .Net que eu recomendo usar em vez de inventar algo novo. Por exemplo, eu frequentemente enfrentei os 2 métodos a seguir para organizar eventos.
Primeiro método:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Eu recomendaria usar este método com cuidado. Se você não o universalizar, poderá eventualmente escrever mais código do que o esperado. Como tal, não definirá uma estrutura mais precisa se comparada com os métodos abaixo.
Pela minha experiência, posso dizer que usei quando comecei a trabalhar com eventos e, consequentemente, fiz papel de bobo. Agora, eu nunca faria isso acontecer.
Segundo método:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Esse método é bastante válido, mas é bom para casos específicos em que o método abaixo não funciona por alguns motivos. Caso contrário, você pode obter muito trabalho monótono.
E agora, vamos dar uma olhada no que já foi criado para os eventos.
Método universal:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Como você pode ver, aqui usamos a classe universal EventHandler. Ou seja, não há necessidade de definir seu próprio manipulador.
Os outros exemplos apresentam o método universal.
Vamos dar uma olhada no exemplo mais simples do gerador 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; } }
Aqui temos uma classe com a propriedade Counter que pode ser alterada de 0 para 10. Com isso, a lógica que altera Counter é processada em uma thread separada.
E aqui está o nosso ponto 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));
}
}
Ou seja, criamos uma instância do nosso gerador, assinamos a mudança do contador e, no manipulador de eventos, enviamos valores para o console.
Aqui está o que obtemos como resultado:
Até agora tudo bem. Mas vamos pensar, em qual thread o manipulador de eventos é executado?
A maioria dos meus colegas respondeu a esta pergunta “Em geral”. Isso significava que nenhum deles não entendia como os delegados são organizados. Vou tentar explicá-lo.
A classe Delegate contém informações sobre um método.
Há também seu descendente, MulticastDelegate, que possui mais de um elemento.
Assim, quando você se inscreve em um evento, uma instância do descendente MulticastDelegate é criada. Cada próximo assinante adiciona um novo método (manipulador de eventos) à instância já criada de MulticastDelegate.
Quando você chama o método Invoke, os manipuladores de todos os assinantes são chamados um a um para seu evento. Com isso, o encadeamento no qual você chama esses manipuladores não sabe nada sobre o encadeamento no qual eles foram especificados e, consequentemente, não pode inserir nada nesse encadeamento.
Em geral, os manipuladores de eventos no exemplo acima são executados na thread gerada no método DoWork(). Ou seja, durante a geração do evento, a thread que o gerou dessa forma, está aguardando a execução de todos os handlers. Eu vou te mostrar isso sem retirar os tópicos de identificação. Para isso, alterei algumas linhas de código no exemplo acima.
Prova de que todos os manipuladores no exemplo acima são executados na thread que chamou o evento
Método onde o evento é gerado
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)); } }
Manipulador
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
No manipulador, enviamos o thread atual para dormir por meio segundo. Se os manipuladores funcionassem no encadeamento principal, esse tempo seria suficiente para um encadeamento gerado em DoWork() terminar seu trabalho e gerar seus resultados.
No entanto, aqui está o que realmente vemos:
Não sei quem e como deve lidar com os eventos gerados pela classe que escrevi, mas não quero que esses manipuladores diminuam o trabalho da minha classe. Por isso, usarei o método BeginInvoke em vez de Invoke. BeginInvoke gera um novo thread.
Observação:os métodos Invoke e BeginInvoke não são membros das classes Delegate ou MulticastDelegate. Eles são os membros da classe gerada (ou a classe universal descrita acima).
Agora, se alterarmos o método no qual o evento é gerado, obteremos o seguinte:
Geração de eventos multiencadeados:
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)); } }
Os dois últimos parâmetros são iguais a null. O primeiro é um retorno de chamada, o segundo é um determinado parâmetro. Não uso callback neste exemplo, pois o exemplo é intermediário. Pode ser útil para feedback. Por exemplo, pode ajudar a classe que gera o evento a determinar se um evento foi tratado e/ou se é necessário obter resultados desse tratamento. Também pode liberar recursos relacionados à operação assíncrona.
Se executarmos o programa, obteremos o seguinte resultado.
Acho que está bem claro que agora os manipuladores de eventos são executados em threads separados, ou seja, o gerador de eventos não se importa com quem, como e por quanto tempo manipulará seus eventos.
E aqui surge a pergunta:e o manuseio sequencial? Afinal, temos o Counter. E se fosse uma mudança em série de estados? Mas não vou responder a esta pergunta, não é assunto deste artigo. Só posso dizer que existem várias maneiras.
E mais uma coisa. Para não repetir as mesmas ações repetidamente, sugiro criar uma classe separada para elas.
Uma classe para geração de eventos assí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); } } }
Neste caso, usamos callback. Ele é executado no mesmo thread que o manipulador. Ou seja, após a conclusão do método do manipulador, o delegado chama h.EndInvoke next.
Aqui está como deve ser usado
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Acho que agora está claro por que o método universal era necessário. Se descrevermos eventos com o método 2, esse truque não funcionará. Caso contrário, você terá que criar a universalidade para seus delegados por conta própria.
Observação :Para projetos reais, recomendo alterar a arquitetura de eventos no contexto de threads. Os exemplos descritos podem prejudicar o trabalho de aplicação com roscas e são fornecidos apenas para fins informativos.
Conclusão
Hope, consegui descrever como os eventos funcionam e onde os manipuladores funcionam. No próximo artigo, pretendo me aprofundar na obtenção de resultados do tratamento de eventos quando uma chamada assíncrona é feita.
Aguardo seus comentários e sugestões.