イベントとストリーム。 パート1

この記事はスレッドに関するものではなく、.NETのスレッドのコンテキストでのイベントに関するものであるとすぐに言わなければなりません。 したがって、スレッドの作業を正しく整理しようとはしません(すべてのロック、コールバック、キャンセルなど)。 フローを適切に編成するために、他の記事があります。



すべての例は、フレームワークバージョン4.0のC#で記述されます(4.6では、すべてがやや単純ですが、4.0にはまだ多くのプロジェクトがあります)。 また、C#5.0に固執しようとします。



まず、.NETのイベントシステムには既に準備ができているデリゲートがあります。ホイールを再発明しないで使用することを強くお勧めします。 たとえば、私はイベントを整理するこれらの2つの方法によく会いました。



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





この方法の使用には注意が必要です。 あなたがそれを普遍化しないなら、あなたは結果としてあなたができるより多くのコードを書くことができます、それは同時に、以下のメソッドの場合よりも明確な構造を定義しません。

私自身の経験から言えば、このようにイベントを扱うようになり、その結果、穴が開いたと言えます。 今はこれを許可していませんが、他の方法はすでに習慣になっています。



方法2
  class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
      
      





この方法には生存権がありますが、以下に説明する方法が何らかの理由で適切でない場合の特別な場合に使用する必要があります。 それ以外の場合は、単調な仕事をたくさん得ることができます。



次に、イベント用にすでに作成されているものについて説明します。



普遍的な方法
  class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
      
      





ご覧のとおり、ここではユニバーサルEventHandlerクラスを使用します。 つまり、独自のハンドラを定義する必要はありません。





さらなる例として、普遍的な方法が使用されます。



イベントジェネレーターの最も単純な例を見てみましょう



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





Counterプロパティを持つクラスがあり、それを0から10に変更できます。さらに、Counterを変更するロジックは別のスレッドで処理されます。



そして、ここがエントリーポイントです。



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





つまり、ジェネレーターのインスタンスを作成し、カウンターの変更をサブスクライブし、イベントハンドラーでコンソールに値を出力します。



それが結果として得られるものです







これまでのところ、すべてがスムーズです。 しかし、イベントハンドラーはどのスレッドで実行されますか?



同僚にこの質問をすることで、「ほぼ」という回答を受け取りました。 これは、私の同僚は誰もデリゲートがどのように機能するか理解していないことを意味しました。 すでにすべてを知っているスポイラーの下のリンゴでこれを説明しようとしますが、読むことはできません。



デリゲートデバイス
Delegateクラスにはメソッド情報があります。

彼の相続人MulticastDelegateもあり、これには複数の要素があります。



そのため、イベントをサブスクライブすると、MulticastDelegateからの継承者のインスタンスが作成されます。 後続の各サブスクライバーは、MulticastDelegateの既に作成されたインスタンスに新しいメソッド(イベントハンドラー)を追加します。



そして、イベントに対してInvokeメソッドを呼び出すと、すべてのサブスクライバーのハンドラーが順番に呼び出され始めます。 同時に、これらのハンドラを呼び出すスレッドは、それらが設定されたスレッドについて何も知らないため、このスレッドに何も入れることはできません。



一般に、上記の例のイベントハンドラーは、DoWork()メソッドによって生成されたスレッドで実行されます。 つまり、イベントが生成されると、この方法でイベントを生成したスレッドは、すべてのハンドラーが完了するまで待機します。 Idスレッドを引き出すことなく、論理的にこれを証明します。 これを行うために、上記の例のいくつかの場所を変更しました



上記の例のすべてのハンドラーがイベントを発生させたスレッドで実行されることの証明
イベントを発生させるメソッド:



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





ハンドラー

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





ハンドラーでは、現在のスレッドを0.5秒間スリープ状態にします。 ハンドラーがメインスレッドで機能する場合、DoWork()で生成されたスレッドが作業を完了して結果を表示するには、この時間で十分です。



しかし、ここに実際に見えるものがあります。







私が書いたクラスによって生成されたイベントを誰がどのように処理するかはわかりませんが、これらのハンドラーがクラスの作業を中断することは望ましくありません。 したがって、Invokeの代わりにBeginInvokeメソッドを使用します。 BeginInvokeは新しいスレッドを生成します。



ご注意
InvokeメソッドとBeginInvokeメソッドはどちらもDelegateクラスまたはMulticastDelegateクラスのメンバーではなく、生成されたクラス(または既に説明したクラスのジェネリッククラス)のメンバーです。


イベントが生成されるメソッドを変更すると、次のようになります



マルチスレッドのイベント生成
  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)); } }
      
      





最後の2つのパラメーターはnullです。 最初はコールバックで、2番目は特定のパラメーターです。 この例は中間的なものなので、この例ではコールバックを使用しません。 たとえば、イベントを生成するクラスがイベントが処理されたかどうかを確認したり、必要に応じてこの処理の結果を取得したり、コメントのプロンプトを表示したり、非同期操作に関連付けられたリソースを解放したりできるように、フィードバックに役立ちます。



プログラムを実行すると、この結果が得られます







今ではイベントハンドラーが個別のスレッドで実行されることを誰もが理解していると思います。 つまり、イベントジェネレーターは、イベントを誰、どのように、どのくらいの期間処理するかが決まりました。



これは別の問題を提起しますが、シーケンシャル処理はどうですか? 同じカウンターがあります。 しかし、それが状態の連続的な変化であった場合はどうでしょうか? しかし、この質問に対する答えは提供しません。これは現在の記事のトピックには関係ありません。 いくつかの方法があるとしか言えません。



まあもっと。 同様のアクションを繰り返し実行しないために、それらを別のクラスに配置することを提案します。



非同期イベントを生成するためのクラス
  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); } } }
      
      





この場合、コールバックを使用します。 ハンドラーと同じスレッドで実行されます。 つまり、ハンドラーメソッドが完了した後、デリゲートは次にh.EndInvokeを呼び出します。



このように使用します



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







普遍的な方法が必要であった理由が(今ではないにしても)明らかになったと思います。 方法2でイベントを記述した場合、そのようなことは機能しません。 さて、または、代理人のために独自に普遍性を作成する必要があります。


更新:
実際のプロジェクトでは、スレッドのコンテキストでイベントのアーキテクチャを変更することをお勧めします。 説明されている例は、スレッドを使用してアプリケーションの作業を損なう可能性があり、実験およびガイドとしてのみ提示されています。



おわりに



イベントがどのように機能し、ハンドラがどこで機能するかについての情報を伝えることができたと思います。 次のパートでは、さらに詳しく調べて、非同期呼び出しでイベント処理の結果を取得する方法について説明します。



コメント、提案、追加、質問を楽しみにしています。



All Articles