Skip to content

MS_ControlInvoke

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

Control.Invoke、.BeginInvoke

概要

「Windows メッセージングキュー(Control.Invoke、.BeginInvoke)」による方式。

補足(なぜこれが必要なのか): 原文が「Windows メッセージング
キューによる方式」と呼んでいる仕組みの前提を補っておく。

【UI スレッド アフィニティ】★
   Windows のウィンドウは【作成したスレッドに属する】
     → そのスレッドのメッセージ ループがメッセージを処理する
     → 【別スレッドから触ってはならない】
        ・[ウィンドウ・システム](MS_WindowSystem)
        ・[プロセス間通信](MS_InterProcessCommunication) の
          「UI は 1 つのスレッドで作る」も同根

【違反するとどうなるか】
   ・.NET Framework 1.x … たまたま動くことがある(が壊れる)
   ・.NET Framework 2.0 以降 … 【InvalidOperationException】★
     「有効ではないスレッド間の操作: コントロール 'xxx' が、
       作成されたスレッド以外のスレッドからアクセスされました。」
【Control.Invoke が行うこと】
   別スレッド                          UI スレッド
     │                                    │
     │ Invoke(delegate)                   │ メッセージ ループが回っている
     ├──【メッセージをキューに積む】──▶ │
     │                                    ├ 取り出して delegate を実行
     │◀────── 完了を待つ ─────────┤
     ▼                                    ▼

 → 【UI スレッド上で実行させる】ための仕組み ★
 → 内部的には PostMessage / SendMessage 相当

詳細

処理方式

この非同期処理方式では、

  • Foreground スレッド(UI スレッド)から Background スレッドを使用して非同期処理を実行し、
  • 非同期処理の実行結果を UI スレッドを使用して画面に反映させる事ができる。

補足(InvokeBeginInvoke の違い): ページ名に両方が並んでいるが、
性質が大きく違うので明示しておく。

Invoke BeginInvoke
呼び出し元 完了を待つ(同期) 待たない(非同期)
戻り値 delegate の戻り値 IAsyncResult
デッドロックの危険 ある 少ない
例外 呼び出し元に伝播する EndInvoke で受け取る
用途 結果が必要なとき 進捗表示など、投げっぱなしでよいとき
// 同期(完了を待つ)
label1.Invoke(new Action(() => label1.Text = "完了"));

// 非同期(待たない)★ 進捗更新はこちらが適切
label1.BeginInvoke(new Action(() => label1.Text = $"{percent}%"));
【Invoke のデッドロック】★ 実務で必ず踏む

   UI スレッド ── 別スレッドの終了を待っている(Thread.Join / lock)
        ▲                                    │
        │                                    │ Invoke で UI スレッドを待つ
        └──────── 相互に待つ ──────────┘
                   → 【フリーズする】

 【対策】
   ・UI スレッドで【ブロックしない】(Join / Wait / .Result を使わない)
   ・進捗通知は BeginInvoke にする
   ・そもそも【async/await にする】(後述)★

InvokeRequired による定型句も押さえておく。

// 従来の定型(自分が UI スレッドかどうかで分岐する)
private void SetText(string text)
{
    if (label1.InvokeRequired)
        label1.Invoke(new Action<string>(SetText), text);
    else
        label1.Text = text;
}
【注意点】
 ・【ハンドルが未作成だと InvokeRequired は false を返す】★
     → まだ表示されていないコントロールで誤判定する
     → IsHandleCreated も併せて確認する
 ・【破棄済みコントロールへの Invoke は例外】
     → フォームを閉じた後に非同期処理が完了すると起きる
     → IsDisposed / CancellationToken で防ぐ

ラッパー・ライブラリ

上記をラップするライブラリ

補足(BackgroundWorker の仕組みと現況): BackgroundWorker
Invoke を隠蔽してくれる点が価値だった。

var worker = new BackgroundWorker { WorkerReportsProgress = true,
                                    WorkerSupportsCancellation = true };
worker.DoWork += (s, e) =>                    // ← 別スレッドで実行される
{
    for (var i = 0; i < 100; i++)
    {
        if (worker.CancellationPending) { e.Cancel = true; return; }
        worker.ReportProgress(i);
    }
};
worker.ProgressChanged += (s, e) =>           // ← 【UI スレッドで実行される】★
    progressBar1.Value = e.ProgressPercentage;
worker.RunWorkerCompleted += (s, e) =>        // ← 【UI スレッドで実行される】
    label1.Text = e.Cancelled ? "中止" : "完了";
worker.RunWorkerAsync();
【BackgroundWorker が優れていた点】
   ・Invoke を書かなくてよい(イベントが UI スレッドで発火する)★
   ・進捗通知・キャンセルの枠組みが最初からある
   ・例外が RunWorkerCompletedEventArgs.Error に入る
     (投げっぱなしでプロセスが落ちない)

【現況】
   ・【今も動く】が、新規では使わない
   ・async/await の方が【簡潔で、合成しやすい】★

移行メモ(現在の書き方):

// async/await(現在の標準)★
private async void btnStart_Click(object sender, EventArgs e)
{
    btnStart.Enabled = false;
    var progress = new Progress<int>(p => progressBar1.Value = p);  // ← UI スレッドへ
    try
    {
        await Task.Run(() => DoWork(progress, _cts.Token), _cts.Token);
        label1.Text = "完了";
    }
    catch (OperationCanceledException) { label1.Text = "中止"; }
    finally { btnStart.Enabled = true; }
}
【なぜ async/await では Invoke が不要なのか】★

   await の後の処理は、
   【元の同期コンテキスト(SynchronizationContext)】に戻される
     → Windows Forms なら WindowsFormsSynchronizationContext
     → その中身は【Control.BeginInvoke】である

 → つまり、async/await は
   【本ページの仕組みを裏で使っている】
 → 手で書かなくてよくなっただけで、原理は変わっていない
【IProgress<T> / Progress<T>】
   ・生成したスレッドの同期コンテキストを捕捉する
   ・Report を呼ぶと【UI スレッドで】コールバックが走る
   → BackgroundWorker.ReportProgress の後継 ★
   → 【UI スレッドで生成する】のが前提(別スレッドで new しない)

async void の注意C10k problem でも触れた):

・イベント ハンドラは async void にせざるを得ない
・それ以外では【async Task にする】
   → async void は例外を捕捉できず、プロセスが落ちる ★

WPF の場合Dispatcher を使う(考え方は同じ)。

Dispatcher.Invoke(() => label1.Content = "完了");        // 同期
await Dispatcher.InvokeAsync(() => label1.Content = "完了");  // 非同期

参考

@IT

Microsoft Learn


Tags: 移行, .NET開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally