Win フォームへのイベント コールバックをスレッド セーフにするにはどうすればよいですか?
-
08-06-2019 - |
質問
フォーム内からオブジェクトのイベントをサブスクライブすると、基本的にコールバック メソッドの制御をイベント ソースに引き渡すことになります。そのイベント ソースが別のスレッドでイベントをトリガーすることを選択するかどうかはわかりません。
問題は、コールバックが呼び出されたときに、フォーム上で更新コントロールを作成できるとは想定できないことです。これは、フォームが実行されたスレッドとは異なるスレッドでイベント コールバックが呼び出された場合、コントロールが例外をスローする場合があるためです。
解決
Simon のコードを少し単純化するには、組み込みの汎用アクション デリゲートを使用できます。これにより、実際には必要のない多数のデリゲート型をコードに散りばめる手間が省けます。また、.NET 3.5 では、Invoke メソッドに params パラメータが追加されたため、一時配列を定義する必要がありません。
void SomethingHappened(object sender, EventArgs ea)
{
if (InvokeRequired)
{
Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea);
return;
}
textBox1.Text = "Something happened";
}
他のヒント
重要な点は次のとおりです。
- UI コントロールの呼び出しを、作成されたスレッド (フォームのスレッド) とは異なるスレッドから行うことはできません。
- デリゲートの呼び出し (つまり、イベント フック) は、イベントを発生させているオブジェクトと同じスレッド上でトリガーされます。
したがって、何らかの作業を実行する別の「エンジン」スレッドがあり、UI (プログレスバーなど) に反映される状態の変化を監視する UI がある場合、問題が発生します。エンジンの火災は、フォームによってフックされたオブジェクト変更イベントです。ただし、フォームがエンジンに登録したコールバック デリゲートは、フォームのスレッドではなく、エンジンのスレッドで呼び出されます。したがって、そのコールバックからコントロールを更新することはできません。ドー!
開始呼び出し 助けに来ます。すべてのコールバック メソッドでこの単純なコーディング モデルを使用するだけで、問題が起こらないことを確認できます。
private delegate void EventArgsDelegate(object sender, EventArgs ea);
void SomethingHappened(object sender, EventArgs ea)
{
//
// Make sure this callback is on the correct thread
//
if (this.InvokeRequired)
{
this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea });
return;
}
//
// Do something with the event such as update a control
//
textBox1.Text = "Something happened";
}
実にシンプルです。
- 使用 呼び出し必須 このコールバックが正しいスレッドで発生したかどうかを確認します。
- そうでない場合は、同じパラメータを使用して正しいスレッドでコールバックを再度呼び出します。メソッドを再度呼び出すには、 呼び出す (ブロック) または 開始呼び出し (ノンブロッキング) メソッド。
- 次回関数が呼び出されるとき、 呼び出し必須 正しいスレッドに到達し、全員が満足しているため、 false が返されます。
これは、この問題に対処し、フォームをマルチスレッド イベント コールバックから安全にするための非常にコンパクトな方法です。
このシナリオでは匿名メソッドをよく使用します。
void SomethingHappened(object sender, EventArgs ea)
{
MethodInvoker del = delegate{ textBox1.Text = "Something happened"; };
InvokeRequired ? Invoke( del ) : del();
}
このトピックに少し遅れましたが、以下を参照してください。 イベントベースの非同期パターン. 。適切に実装すると、イベントが常に UI スレッドから発生することが保証されます。
以下に、同時呼び出しを 1 つだけ許可する簡単な例を示します。複数の呼び出し/イベントをサポートするには、もう少し配管が必要です。
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public class MainForm : Form
{
private TypeWithAsync _type;
[STAThread()]
public static void Main()
{
Application.EnableVisualStyles();
Application.Run(new MainForm());
}
public MainForm()
{
_type = new TypeWithAsync();
_type.DoSomethingCompleted += DoSomethingCompleted;
var panel = new FlowLayoutPanel() { Dock = DockStyle.Fill };
var btn = new Button() { Text = "Synchronous" };
btn.Click += SyncClick;
panel.Controls.Add(btn);
btn = new Button { Text = "Asynchronous" };
btn.Click += AsyncClick;
panel.Controls.Add(btn);
Controls.Add(panel);
}
private void SyncClick(object sender, EventArgs e)
{
int value = _type.DoSomething();
MessageBox.Show(string.Format("DoSomething() returned {0}.", value));
}
private void AsyncClick(object sender, EventArgs e)
{
_type.DoSomethingAsync();
}
private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e)
{
MessageBox.Show(string.Format("DoSomethingAsync() returned {0}.", e.Value));
}
}
class TypeWithAsync
{
private AsyncOperation _operation;
// synchronous version of method
public int DoSomething()
{
Thread.Sleep(5000);
return 27;
}
// async version of method
public void DoSomethingAsync()
{
if (_operation != null)
{
throw new InvalidOperationException("An async operation is already running.");
}
_operation = AsyncOperationManager.CreateOperation(null);
ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore);
}
// wrapper used by async method to call sync version of method, matches WaitCallback so it
// can be queued by the thread pool
private void DoSomethingAsyncCore(object state)
{
int returnValue = DoSomething();
var e = new DoSomethingCompletedEventArgs(returnValue);
_operation.PostOperationCompleted(RaiseDoSomethingCompleted, e);
}
// wrapper used so async method can raise the event; matches SendOrPostCallback
private void RaiseDoSomethingCompleted(object args)
{
OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args);
}
private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e)
{
var handler = DoSomethingCompleted;
if (handler != null) { handler(this, e); }
}
public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted;
}
public class DoSomethingCompletedEventArgs : EventArgs
{
private int _value;
public DoSomethingCompletedEventArgs(int value)
: base()
{
_value = value;
}
public int Value
{
get { return _value; }
}
}
}
として lazy programmer
, 私はこれを非常に怠惰な方法で実行しています。
私がやっていることはただこれだけです。
private void DoInvoke(MethodInvoker del) {
if (InvokeRequired) {
Invoke(del);
} else {
del();
}
}
//example of how to call it
private void tUpdateLabel(ToolStripStatusLabel lbl, String val) {
DoInvoke(delegate { lbl.Text = val; });
}
DoInvoke を関数内にインライン化することも、別の関数内に非表示にして、面倒な作業を行うこともできます。
関数を DoInvoke メソッドに直接渡すことができることに注意してください。
private void directPass() {
DoInvoke(this.directInvoke);
}
private void directInvoke() {
textLabel.Text = "Directly passed.";
}
多くの単純なケースでは、MethodInvoker デリゲートを使用すると、独自のデリゲート型を作成する必要がなくなります。