この記事は特にスレッドではなく、.NETのスレッドのコンテキストでのイベントに関係することを簡単に説明します。そのため、スレッドを正しく配置しようとはしません(すべてのブロック、コールバック、キャンセルなど)。このテーマに関する記事はたくさんあります。
すべての例は、フレームワークバージョン4.0のC#で記述されています(4.6では、すべてが少し簡単ですが、4.0には多くのプロジェクトがあります)。また、C#バージョン5.0に固執しようとします。
まず、新しいものを発明する代わりに使用することを強くお勧めする.Netイベントシステムの準備ができている代理人がいることに注意したいと思います。たとえば、私はイベントを整理するために次の2つの方法に頻繁に直面しました。
最初の方法:
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; } }
ここに、0から10に変更できるCounterプロパティを持つクラスがあります。その時点で、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メソッドを呼び出すと、すべてのサブスクライバーのハンドラーがイベントに対して1つずつ呼び出されます。そのとき、これらのハンドラーを呼び出すスレッドは、それらが指定されたスレッドについて何も知らず、それに応じて、そのスレッドに何も挿入できません。
一般に、上記の例のイベントハンドラーは、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.EndInvokenextを呼び出します。
使用方法は次のとおりです
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
普遍的な方法が必要だった理由は今や明らかだと思います。方法2でイベントを説明する場合、このトリックは機能しません。それ以外の場合は、自分で代表者の普遍性を作成する必要があります。
注 :実際のプロジェクトでは、スレッドのコンテキストでイベントアーキテクチャを変更することをお勧めします。説明されている例は、スレッドを使用したアプリケーションの作業に損傷を与える可能性があり、情報提供のみを目的として提供されています。
結論
うまくいけば、イベントがどのように機能し、ハンドラーがどこで機能するかを説明することができました。次の記事では、非同期呼び出しが行われたときにイベント処理の結果を取得する方法について詳しく説明する予定です。
コメントや提案をお待ちしております。