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