はじめに
WinForms でマルチスレッド処理を行っていると、
ほぼ必ず登場するのが Invoke / BeginInvoke です。
ネットで調べると、
- Invoke は同期
- BeginInvoke は非同期
- UI スレッドに処理を渡す
という説明は多く見つかります。
しかし実際に使ってみると、次のような疑問を持ちました。
- なぜ BeginInvoke を使うと UI が詰まるのか?
- delegate.Invoke とは何が違うのか?
この記事では、Invoke / BeginInvoke を起点に、
「UI スレッドへ処理を戻す方法」を整理・比較します。
Invoke / BeginInvoke の基本
WinForms では、UI オブジェクトは
作成されたスレッド(通常は UI スレッド)でしか操作できません。
そのため、別スレッドから UI を更新する場合、
次のように書きます。
label1.Invoke(() =>
{
label1.Text = "更新";
});
「 UI スレッドに渡す 」の正確な意味
よく「Invoke は UI スレッドに処理を渡す」と説明されますが、
正確には次の意味です。
Invoke は、その Control が作成されたスレッドのメッセージキューに処理を積む
WinForms では通常、Control は UI スレッドで作成される。
UI スレッドはメッセージループを持つため、
結果的に UI スレッドで実行されるだけです。
👉 実務上は
「Control.Invoke = UI スレッドに渡す」
と理解して問題ありません。
Invoke / BeginInvoke の違い
Invoke
UI スレッドのメッセージキューに処理を積み、
その処理が実行されるまで呼び出し元は待機します。
そのため、
- 呼び出し側の処理は一時停止される
- 結果として UI スレッドへの投入量が自然に制御される
ループ内で使っても、
UI スレッドが過剰に詰まることは起きにくい です。
BeginInvoke
UI スレッドのメッセージキューに処理を積み、
処理の実行完了を待たず即座に復帰します。
そのため、
- 呼び出し側は高速に回り続ける
- UI スレッドのキューに処理が溜まりやすい
ループ内で多用すると、
UI スレッドが処理しきれず、重く感じる原因 になります。
delegate.Invoke
Action action = () => Console.WriteLine("Hello");
action.Invoke();
これは何をしているか?
スレッド切り替えは一切しない
単なる 同期メソッド呼び出し
👉 名前が同じだけで意味は完全に別
Task / async / await(モダンな代替手段)
Invoke を直接使わなくても、
Task / async / await を使えば自然に UI スレッドへ戻れます。
private async void button1_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
// 重い処理
});
// ここは UI スレッド
label1.Text = "完了";
}
なぜ UI スレッドに戻るのか?
WinForms では SynchronizationContext が UI スレッドに紐づいている。
await 後は、その Context に自動で戻る。
向いている用途
- ユーザー操作を起点とする処理
- 一連の処理の流れを分かりやすく書きたい場合
向いていない用途
- 実行タイミングそのものが意味を持つ処理
- 高頻度・連続的に繰り返される処理
まとめ
処理の性質(単発か、連続か、タイミング制御が必要か)に応じて、
適切な手段を選ぶことが重要です。