Control.Invoke()を用いることでスレッド セーフとできますが、スレッドが同一ならばこのメソッドを介する必要はないため、InvokeRequiredで確認した上で呼び出します。
※1 破棄されているコントロールからInvoke()を呼び出すと、「破棄されたオブジェクトにアクセスできません。」としてObjectDisposedExceptionが投げられます。コントロールが破棄されていることはIsDisposedで事前に確認できますが、確認してからInvoke()を呼ぶまでの間に他のスレッドで破棄される恐れがあるため、ObjectDisposedExceptionを捕捉するのが確実です。
※2 異なるスレッドからコントロールにアクセスすると、「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'control' がアクセスされました。(Cross-thread operation not valid: Control 'control' accessed from a thread other than the thread it was created on.)」としてInvalidOperationExceptionが投げられます。このときの「コントロールが作成されたスレッド」とはコントロールがnewで作成されたスレッドではなく、コントロールのハンドルが作成されたスレッドになります。
通常、コントロールにハンドルが関連付けられているか、Control.IsHandleCreatedで確認する必要はありません。この確認はControl.InvokeRequired内で行われ、関連付けられていないならば親コントロールのハンドルからスレッドが判定されます。InvokeRequired - Control.cs
ただし親コントロールに追加していない、つまりControl.Parentの上位の階層までnullとなるコントロールは注意が必要です。そのときControl.InvokeRequiredはfalseを返すため、結果として異なるスレッドからアクセスすることになりますがInvalidOperationExceptionは投げられません。この状況を回避するには、コントロールの属するフォームのLoadイベントが発生するまで、バックグラウンドのスレッドを作成するのを待機します。Remarks - Control.InvokeRequired Property (System.Windows.Forms) | Microsoft Learn
// Windowsフォームコントロールに対して非同期な呼び出しを行うためのデリゲート
delegate void SetTextCallback( string text );
private void SetText( string text )
{
// 呼び出し元のコントロールのスレッドと異なるかを確認する
if (control.InvokeRequired)
{
// このブロックは、別スレッドで実行される
// 同一メソッドへのコールバックを作成する
SetTextCallback delegateMethod = new SetTextCallback(SetText);
// コントロールのInvoke()メソッドを呼び出すことで、コントロールのスレッドでこのメソッドを実行する
control.Invoke(delegateMethod, new object[] { text });
}
else
{
// このブロックは、同一スレッドで実行される
// コントロールを直接呼び出す
control.Text = text;
}
}
MethodInvokerを用いればデリゲートの定義を省略でき、これは次のようにも記述できます。
private void SetText(string text)
{
if (control.InvokeRequired)
{
control.Invoke((MethodInvoker)delegate { SetText(text); });
}
else
{
control.Text = text;
}
}
またはMethodInvokerのメソッドをローカルに定義すれば、再帰呼び出しすることなく記述できます。
MethodInvoker method = () =>
{
// コントロールに対する処理
};
if (control.InvokeRequired)
{
control.Invoke(method);
}
else
{
method();
}
別スレッドから呼ばれることが確実ならば、InvokeRequiredを省略してデリゲートのメソッド内に記述できます。
control.Invoke((MethodInvoker)delegate
{
// コントロールに対する処理
});
戻り値があるならば、Invoke()に渡すデリゲートをFunc<TResult>などにします。
int result = (int)control.Invoke((Func<int>)delegate
{
return 1;
});
InvokeRequiredにより分岐する処理は、次のように拡張メソッドにまとめられます。c# - Automating the InvokeRequired code pattern - Stack Overflow
public static void InvokeIfRequired(this Control control, Action method)
{
if (control.InvokeRequired)
{
control.Invoke(method);
}
else
{
method();
}
}
public static T InvokeIfRequired<T>(this Control control, Func<T> method)
{
if (control.InvokeRequired)
{
return (T)control.Invoke(method);
}
else
{
return method();
}
}
マルチスレッドによる処理はBackgroundWorkerを用いることで、コントロールへのアクセスが容易になります。このクラスはそれぞれの処理の過程で、次のように異なるスレッドでイベントを発生します。
private BackgroundWorker backgroundWorker;
{
backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.DoWork += BackgroundWorker_DoWork;
backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
// ...
int data = 123;
if (!backgroundWorker.IsBusy)
{
backgroundWorker.RunWorkerAsync(data);
// このRunWorkerAsyncメソッドの呼び出しによりDoWorkイベントが発生するが、
// ハンドラは別スレッドで呼び出されるため、いつ呼ばれるかはわからない
}
}
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { // このメソッドは、ワーカースレッドで実行される BackgroundWorker worker = (BackgroundWorker)sender; int data = (int)e.Argument; int max = 150; for (int i = 0; i < max; i++) { data++; System.Threading.Thread.Sleep(50); worker.ReportProgress(100 * i / max); // このReportProgressメソッドの呼び出しによりProgressChangedイベントが発生するが、 // ハンドラは別スレッドで呼び出されるため、いつ呼ばれるかはわからない // WorkerReportsProgressがtrueではないと、 // ここでInvalidOperationExceptionが投げられRunWorkerCompletedイベントが発生する } e.Result = data; // このDoWorkイベント ハンドラを抜けると、RunWorkerCompletedイベントが発生する }
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { Console.Write(e.ProgressPercentage); }
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { // エラーが発生した } else if (e.Cancelled) { // キャンセルされた } else { int result = (int)e.Result; Console.Write(result); } }
ラムダ式を用いると、これらは次のようにも記述できます。
backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.DoWork += (object sender, DoWorkEventArgs e) => { };
backgroundWorker.ProgressChanged += (object sender, ProgressChangedEventArgs e) => { };
backgroundWorker.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => { };
if (!backgroundWorker.IsBusy)
{
backgroundWorker.RunWorkerAsync();
}
スレッド セーフな処理を、Windows Form以外にも適用できます。ただし実行環境によって、動作に相違があります。SynchronizationContext の実装に関する注意事項 - MSDN マガジン: 並列コンピューティング - SynchronizationContext こそすべて | Microsoft Learn
Task.ContinueWith()にTaskScheduler.FromCurrentSynchronizationContext()で取得できるTaskSchedulerを渡すことで、UIが作成されたのと同じスレッドで指定の処理を実行できます。
// ここはUIスレッド Task.Run(() => { // }).ContinueWith((_task) => { // ここもUIスレッド }, TaskScheduler.FromCurrentSynchronizationContext());
ただしこの方法は、UIスレッド以外からは使用できません。呼び出すと「現在の SynchronizationContext を TaskScheduler として使用することはできません。」としてInvalidOperationExceptionが投げられます。
// ここはUIスレッド Task.Run(() => { // ここはTaskのスレッド Task.Run(() => { // }).ContinueWith((_task) => { // }, TaskScheduler.FromCurrentSynchronizationContext()); // InvalidOperationException });