C#のBeginInvokeとは?Invokeとの違い・使い方・UIスレッド更新まで初心者向けに解説

はじめに

C#でWindows FormsやWPFなどのデスクトップアプリを作っていると、BeginInvokeというメソッドを見かけることがあります。

特に、バックグラウンド処理から画面のLabelTextBoxを更新しようとしたときに、

C#
有効ではないスレッド間の操作です

または、

C#
クロススレッド操作が有効ではありません

といったエラーに遭遇し、調べているうちにInvokeBeginInvokeにたどり着く人は多いでしょう。

BeginInvokeは、簡単に言うと「別スレッドからUIスレッドへ処理を依頼するための仕組み」です。特にWindows Formsでは、画面部品であるコントロールを安全に更新するためによく使われます。

この記事では、C#のBeginInvokeについて、Invokeとの違い、基本的な使い方、UIスレッド更新の考え方、よくあるエラーと対処法まで初心者向けに解説します。

1. C#のBeginInvokeとは?まず押さえたい基本

1-1. BeginInvokeの役割を初心者向けに一言で解説

C#のBeginInvokeは、一言でいうと「指定した処理を、対象のスレッドで非同期に実行してもらうためのメソッド」です。

Windows Formsでよく使うControl.BeginInvokeの場合、主な目的は「UIスレッドに処理を渡すこと」です。

たとえば、別スレッドで重い処理を実行している途中に、画面上のラベルへ進捗を表示したいとします。しかし、Windows FormsではUIコントロールを作成したスレッド、つまり通常はUIスレッドからしか安全に操作できません。

そこで、次のようにBeginInvokeを使います。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "処理中です...";
}));

このコードは、「label1.Textを変更する処理をUIスレッド側で実行してください」と依頼しているイメージです。

1-2. BeginInvokeが必要になる代表的な場面

BeginInvokeが必要になる代表的な場面は、次のようなケースです。

バックグラウンド処理中に画面を更新したい場合です。たとえば、Task.RunThreadで時間のかかる処理を別スレッドに逃がし、その途中で進捗率やメッセージを画面に表示したいときに使います。

また、外部ライブラリやイベントコールバックがUIスレッド以外から呼ばれる場合にも使われます。たとえば、通信処理、ファイル監視、タイマー処理、非同期イベントなどからUIを更新する場合です。

直接UIコントロールを操作するとクロススレッド例外が発生するため、BeginInvokeを使ってUIスレッドへ処理を渡します。

1-3. BeginInvokeは「非同期に処理を依頼する」仕組み

BeginInvokeの重要なポイントは、「非同期に依頼する」という点です。

BeginInvokeを呼び出した側は、依頼した処理の完了を待たずに次の処理へ進みます。つまり、呼び出し元スレッドはブロックされません。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新しました";
}));

// BeginInvokeで依頼した処理の完了を待たずに、ここへ進む
Console.WriteLine("BeginInvokeの呼び出し後");

このように、BeginInvokeは「あとでUIスレッドが実行できるタイミングで、この処理を実行しておいてください」という予約に近い動きをします。

1-4. BeginInvokeがよく登場するWindows Forms/WPFの文脈

BeginInvokeは、特にWindows FormsとWPFの文脈でよく登場します。

Windows Formsでは、主に次のように使います。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "Windows Formsで更新";
}));

これはControl.BeginInvokeです。FormControlを継承しているため、フォーム上でthis.BeginInvokeと書けます。

一方、WPFではControl.BeginInvokeではなく、Dispatcher.BeginInvokeを使います。

C#
Dispatcher.BeginInvoke(new Action(() =>
{
textBlock1.Text = "WPFで更新";
}));

どちらも目的は似ています。UIスレッド以外からUIを安全に更新するために、UIスレッドへ処理を渡します。

2. InvokeとBeginInvokeの違い

2-1. Invokeは同期実行、BeginInvokeは非同期実行

InvokeBeginInvokeの最大の違いは、同期実行か非同期実行かです。

Invokeは同期実行です。呼び出した側は、依頼した処理が完了するまで待ちます。

C#
this.Invoke((Action)(() =>
{
label1.Text = "Invokeで更新";
}));

// UI更新が完了してからここへ進む

一方、BeginInvokeは非同期実行です。呼び出した側は、依頼した処理の完了を待たずに次へ進みます。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "BeginInvokeで更新";
}));

// UI更新の完了を待たずにここへ進む

つまり、確実に処理完了を待ちたいならInvoke、処理を依頼して先へ進みたいならBeginInvokeという違いがあります。

2-2. 呼び出し元スレッドが待機するかどうかの違い

Invokeでは、呼び出し元スレッドが待機します。

たとえば、バックグラウンドスレッドからInvokeを呼ぶと、そのバックグラウンドスレッドはUIスレッド上で処理が完了するまで待ちます。

C#
this.Invoke((Action)(() =>
{
label1.Text = "完了を待つ";
}));

これに対して、BeginInvokeでは呼び出し元スレッドは待機しません。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "完了を待たない";
}));

そのため、バックグラウンド処理を止めずにUI更新だけを予約したい場合には、BeginInvokeが向いています。

2-3. 実行順序と戻り値の扱いの違い

Invokeは、呼び出した処理が完了してから次へ進むため、処理順序を把握しやすいです。また、戻り値がある処理も扱いやすくなります。

C#
string text = (string)this.Invoke(new Func<string>(() =>
{
return textBox1.Text;
}));

この例では、UIスレッド上でtextBox1.Textを取得し、その結果を呼び出し元で受け取っています。

一方、BeginInvokeは非同期に処理を予約するため、基本的には「戻り値をすぐ受け取る」用途には向きません。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "非同期に更新";
}));

BeginInvokeは、結果を待つというより「UI更新を依頼する」使い方が中心です。

2-4. デッドロックを避けたいときにBeginInvokeが使われる理由

Invokeは同期実行なので、使い方によってはデッドロックの原因になることがあります。

たとえば、UIスレッドがバックグラウンドスレッドの完了を待っていて、そのバックグラウンドスレッドがInvokeでUIスレッドの処理完了を待っている場合、お互いに待ち続ける状態になる可能性があります。

イメージとしては次のような状態です。

C#
// UIスレッド側
task.Wait(); // バックグラウンド処理の完了を待つ

// バックグラウンドスレッド側
this.Invoke((Action)(() =>
{
label1.Text = "更新";
}));

UIスレッドはtask.Wait()で待機しています。一方、バックグラウンドスレッドはInvokeでUIスレッドの実行を待っています。すると、どちらも先へ進めません。

BeginInvokeは処理完了を待たずに戻るため、このような待ち合いを避けやすくなります。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));

ただし、BeginInvokeを使えば必ずデッドロックが解決するわけではありません。UIスレッドをWait()Resultでブロックしない設計も重要です。

2-5. InvokeとBeginInvokeの使い分け早見表

比較項目InvokeBeginInvoke
実行方式同期実行非同期実行
呼び出し元処理完了まで待つ処理完了を待たない
戻り値扱いやすいすぐには扱いにくい
UI更新可能可能
デッドロック起きる可能性がある回避しやすい
主な用途結果を待ちたい処理UI更新を依頼したい処理

初心者のうちは、バックグラウンドスレッドから画面表示を更新するだけならBeginInvoke、UIから値を取得して結果を待つ必要があるならInvokeと考えると理解しやすいです。

3. BeginInvokeが必要になる理由:UIスレッドとスレッドセーフ

3-1. C#のUIアプリではUI更新はUIスレッドで行う必要がある

Windows FormsやWPFなどのUIアプリでは、画面部品の操作は基本的にUIスレッドで行う必要があります。

UIスレッドとは、画面の描画やユーザー操作の処理を担当しているスレッドです。ボタンのクリックイベント、テキストボックスへの入力、フォームの表示などは通常このUIスレッドで処理されます。

たとえば、次のようなコードはボタンクリックイベント内で実行されるため、通常はUIスレッド上で動きます。

C#
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "ボタンがクリックされました";
}

この場合は問題ありません。

しかし、Task.RunThreadを使って別スレッドで処理を実行し、その中から直接label1.Textを変更しようとすると問題が起きます。

3-2. 別スレッドからコントロールを直接操作するとエラーになる理由

Windows Formsのコントロールは、基本的に作成されたスレッドから操作される前提で作られています。

そのため、別スレッドから直接操作すると、コントロール内部の状態が不整合になる可能性があります。たとえば、UIスレッドが描画中のタイミングで、別スレッドが同じコントロールの値を書き換えると、予期しない動作につながる可能性があります。

そのような問題を防ぐため、Windows Formsではデバッグ時にクロススレッド操作を検出し、例外を発生させます。

C#
Task.Run(() =>
{
// これはNG
label1.Text = "別スレッドから直接更新";
});

このようなコードを書くと、次のような例外が発生することがあります。

C#
InvalidOperationException:
有効ではないスレッド間の操作です。

3-3. 「有効ではないスレッド間の操作です」が発生する原因

「有効ではないスレッド間の操作です」というエラーは、UIスレッド以外のスレッドからUIコントロールを直接操作したときに発生します。

よくある原因は次のようなコードです。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(1000);
label1.Text = "完了しました"; // ここでエラーになる可能性がある
});
}

button1_Click自体はUIスレッドで実行されます。しかし、Task.Runの中身は別スレッドで実行されます。そのため、Task.Run内からlabel1.Textを直接変更すると、クロススレッド操作になります。

この問題を解決するために、BeginInvokeを使います。

3-4. BeginInvokeでUIスレッドに処理を渡す仕組み

BeginInvokeを使うと、別スレッドからUIスレッドへ処理を渡せます。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(1000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "完了しました";
}));
});
}

このコードでは、Task.Runの中で重い処理を実行し、UI更新だけをBeginInvokeでUIスレッドに依頼しています。

つまり、次のような流れです。

  1. ボタンをクリックする

  2. 重い処理をバックグラウンドスレッドで実行する

  3. UI更新が必要になったらBeginInvokeを呼ぶ

  4. UIスレッドがlabel1.Textを更新する

このように、BeginInvokeはスレッドセーフにUIを更新するための橋渡し役になります。

4. BeginInvokeの基本的な使い方

4-1. Windows FormsでBeginInvokeを使う基本コード

Windows FormsでBeginInvokeを使う基本形は次のとおりです。

C#
this.BeginInvoke((Action)(() =>
{
// UIスレッドで実行したい処理
}));

フォーム内で使う場合、thisはフォーム自身を指します。フォームはControlを継承しているため、BeginInvokeを呼び出せます。

たとえば、ラベルを更新する場合は次のようになります。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新しました";
}));

ポイントは、UIコントロールを操作する処理をBeginInvokeの中に書くことです。

4-2. LabelやTextBoxを別スレッドから更新する例

次の例では、バックグラウンド処理からLabelTextBoxを更新しています。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(1000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "処理が完了しました";
textBox1.Text = "完了";
}));
});
}

このコードでは、Thread.Sleep(1000)で1秒待機したあと、BeginInvokeを使ってUIスレッド上でlabel1textBox1を更新しています。

直接次のように書くのは避けましょう。

C#
Task.Run(() =>
{
label1.Text = "処理が完了しました"; // NG
});

UIコントロールを操作する処理は、UIスレッドへ渡す必要があります。

4-3. ラムダ式を使ったシンプルな書き方

現在のC#では、ラムダ式を使ってBeginInvokeを書くのが一般的です。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "ラムダ式で更新";
}));

1行で書くこともできます。

C#
this.BeginInvoke((Action)(() => label1.Text = "更新"));

複数のUI操作をまとめたい場合は、ブロック形式で書くと読みやすくなります。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "処理中";
button1.Enabled = false;
progressBar1.Value = 50;
}));

ラムダ式を使うと、別途メソッドやデリゲートを定義しなくても簡潔に書けます。

4-4. delegateを使った従来の書き方

ラムダ式が一般的になる前は、delegateを使った書き方もよく使われていました。

C#
this.BeginInvoke(new MethodInvoker(delegate
{
label1.Text = "delegateで更新";
}));

または、メソッドを別に用意して渡す書き方もあります。

C#
private void UpdateLabel()
{
label1.Text = "メソッドで更新";
}

private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
this.BeginInvoke(new MethodInvoker(UpdateLabel));
});
}

どちらも意味は同じです。UIスレッドで実行したい処理をBeginInvokeに渡しています。

初心者には、まずラムダ式の書き方を覚えるのがおすすめです。

4-5. InvokeRequiredと組み合わせる基本パターン

Windows Formsでは、現在のスレッドがUIスレッドかどうかを判定するためにInvokeRequiredを使います。

基本パターンは次のようになります。

C#
private void SetLabelText(string text)
{
if (label1.InvokeRequired)
{
label1.BeginInvoke((Action)(() =>
{
label1.Text = text;
}));
}
else
{
label1.Text = text;
}
}

このメソッドは、UIスレッドから呼ばれた場合はそのままlabel1.Textを更新し、別スレッドから呼ばれた場合はBeginInvokeでUIスレッドに処理を渡します。

使う側は、スレッドをあまり意識せずに呼び出せます。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
SetLabelText("バックグラウンド処理中です");
});
}

この形は実務でもよく使われる基本パターンです。

5. 実践例:バックグラウンド処理からUIを更新する

5-1. TaskやThreadからUIを更新したいケース

重い処理をUIスレッドで直接実行すると、画面が固まります。

たとえば、次のようなコードです。

C#
private void button1_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
label1.Text = "完了しました";
}

このコードでは、5秒間UIスレッドが止まります。その間、フォームの移動、ボタン操作、画面再描画などができなくなります。

そこで、重い処理はTask.Runでバックグラウンドスレッドに逃がし、UI更新だけをBeginInvokeでUIスレッドに戻します。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(5000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "完了しました";
}));
});
}

これにより、重い処理中でもUIスレッドをブロックしにくくなります。

5-2. 処理中メッセージや進捗率を画面に表示する例

次の例では、バックグラウンド処理の進捗率をProgressBarLabelに表示します。

C#
private void button1_Click(object sender, EventArgs e)
{
progressBar1.Value = 0;
label1.Text = "処理を開始します";

Task.Run(() =>
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);

int progress = i;

this.BeginInvoke((Action)(() =>
{
progressBar1.Value = progress;
label1.Text = $"{progress}% 完了";
}));
}

this.BeginInvoke((Action)(() =>
{
label1.Text = "すべての処理が完了しました";
}));
});
}

ポイントは、progressというローカル変数に値を退避してからBeginInvokeの中で使っていることです。

ループ中の変数を扱う場合、意図しない値が表示されないように、UI更新に使う値を明確にしておくと安全です。

5-3. Buttonクリック後に重い処理を実行する例

実際のアプリでは、ボタンをクリックしたあとに重い処理を開始し、処理中はボタンを無効化し、完了後に再度有効化することがよくあります。

C#
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
label1.Text = "処理中です...";

Task.Run(() =>
{
try
{
// 重い処理の例
Thread.Sleep(3000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "処理が完了しました";
}));
}
catch (Exception ex)
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "エラーが発生しました";
MessageBox.Show(ex.Message);
}));
}
finally
{
this.BeginInvoke((Action)(() =>
{
button1.Enabled = true;
}));
}
});
}

この例では、重い処理をTask.Runで実行し、UI更新はすべてBeginInvoke内で行っています。

try-catch-finallyを使うことで、エラーが起きてもボタンを再度有効化できます。

5-4. BeginInvokeで画面のフリーズを避ける考え方

BeginInvokeは、UI更新をUIスレッドに渡すための仕組みです。ただし、BeginInvoke自体が重い処理を別スレッドで実行してくれるわけではありません。

次のように書いても、重い処理がUIスレッド上で実行されるため、画面が固まる可能性があります。

C#
this.BeginInvoke((Action)(() =>
{
Thread.Sleep(5000); // UIスレッドで実行されるためNG
label1.Text = "完了";
}));

BeginInvokeの中には、基本的にUI更新などの短い処理だけを書くべきです。

重い処理はTask.Runなどでバックグラウンドに逃がし、画面更新だけをBeginInvokeでUIスレッドに戻す、という分担が大切です。

6. BeginInvokeを使うときの注意点

6-1. コントロールが破棄済みの場合に発生する例外

BeginInvokeを使うときに注意したいのが、コントロールやフォームがすでに破棄されているケースです。

たとえば、バックグラウンド処理中にユーザーがフォームを閉じた場合、その後にBeginInvokeでUI更新しようとすると例外が発生することがあります。

C#
Task.Run(() =>
{
Thread.Sleep(3000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));
});

3秒待っている間にフォームが閉じられると、thislabel1がすでに破棄されている可能性があります。

このような場合は、IsDisposedDisposingを確認します。

6-2. IsDisposedやDisposingを確認する必要があるケース

フォームやコントロールが破棄されている可能性がある場合は、BeginInvokeの前に状態を確認します。

C#
if (!this.IsDisposed && !this.Disposing)
{
this.BeginInvoke((Action)(() =>
{
if (!label1.IsDisposed)
{
label1.Text = "更新しました";
}
}));
}

ただし、チェックした直後に破棄される可能性も完全にはゼロではありません。そのため、必要に応じて例外処理も組み合わせます。

C#
try
{
if (!this.IsDisposed && !this.Disposing)
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新しました";
}));
}
}
catch (InvalidOperationException)
{
// フォーム終了時などは必要に応じて無視する
}
catch (ObjectDisposedException)
{
// 破棄済みの場合の処理
}

フォームを閉じる可能性があるアプリでは、このような対策が重要です。

6-3. BeginInvokeを大量に呼び出すと処理が詰まることがある

BeginInvokeは便利ですが、短時間に大量に呼び出すとUIスレッド側の処理が詰まることがあります。

たとえば、ループの中で非常に高頻度にBeginInvokeを呼ぶと、UI更新の依頼がキューにたまり、画面の反映が遅くなることがあります。

C#
for (int i = 0; i < 100000; i++)
{
int value = i;

this.BeginInvoke((Action)(() =>
{
label1.Text = value.ToString();
}));
}

このようなコードは避けるべきです。

進捗表示であれば、毎回更新するのではなく、一定間隔ごとに更新するのがおすすめです。

C#
for (int i = 0; i <= 100000; i++)
{
if (i % 1000 == 0)
{
int value = i;

this.BeginInvoke((Action)(() =>
{
label1.Text = value.ToString();
}));
}
}

UI更新は、人間が見て分かる程度の頻度で十分です。

6-4. 例外処理が呼び出し元に直接伝わらない点に注意

BeginInvokeは非同期に処理を依頼するため、BeginInvoke内で発生した例外が呼び出し元のtry-catchでそのまま捕まるとは限りません。

C#
try
{
this.BeginInvoke((Action)(() =>
{
throw new Exception("エラー");
}));
}
catch (Exception ex)
{
// ここで捕まるとは限らない
MessageBox.Show(ex.Message);
}

そのため、BeginInvokeの中で例外が起きる可能性がある場合は、処理の中にtry-catchを書きます。

C#
this.BeginInvoke((Action)(() =>
{
try
{
// UI更新処理
label1.Text = "更新";
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}));

非同期実行では、例外の扱いに注意が必要です。

6-5. 戻り値が必要な処理には向かない場合がある

BeginInvokeは非同期に処理を依頼するため、戻り値をすぐ使いたい処理には向きません。

たとえば、UIスレッド上のTextBoxから値を取得して、その結果をすぐに使いたい場合はInvokeのほうが分かりやすいです。

C#
string text = (string)this.Invoke(new Func<string>(() =>
{
return textBox1.Text;
}));

一方、BeginInvokeでは処理の完了を待たないため、次のような考え方になります。

C#
this.BeginInvoke((Action)(() =>
{
string text = textBox1.Text;
label1.Text = text;
}));

戻り値が必要なのか、単にUI更新を依頼したいだけなのかを考えて使い分けましょう。

7. BeginInvokeでよくあるエラーと解決策

7-1. 「InvokeまたはBeginInvokeは、ウィンドウハンドルが作成されるまで呼び出せません」の原因

Windows Formsで次のようなエラーが出ることがあります。

C#
InvokeまたはBeginInvokeは、ウィンドウハンドルが作成されるまで呼び出せません。

これは、対象コントロールのウィンドウハンドルがまだ作成されていない状態でBeginInvokeを呼んだ場合に発生します。

たとえば、フォームのコンストラクタ内など、まだ画面表示の準備が完了していないタイミングでBeginInvokeを呼ぶと起こることがあります。

C#
public Form1()
{
InitializeComponent();

this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));
}

このような場合は、LoadイベントやShownイベントなど、フォームのハンドルが作成された後のタイミングで実行します。

C#
private void Form1_Shown(object sender, EventArgs e)
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "フォーム表示後に更新";
}));
}

また、IsHandleCreatedを確認する方法もあります。

C#
if (this.IsHandleCreated)
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));
}

7-2. 「クロススレッド操作が有効ではありません」の解決方法

「クロススレッド操作が有効ではありません」というエラーは、別スレッドからUIコントロールを直接操作していることが原因です。

NG例は次のとおりです。

C#
Task.Run(() =>
{
label1.Text = "更新"; // NG
});

解決するには、BeginInvokeを使ってUIスレッドへ処理を渡します。

C#
Task.Run(() =>
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));
});

または、InvokeRequiredを使って共通メソッド化します。

C#
private void UpdateLabel(string message)
{
if (label1.InvokeRequired)
{
label1.BeginInvoke((Action)(() => UpdateLabel(message)));
return;
}

label1.Text = message;
}

この形にしておくと、UIスレッドから呼ばれても別スレッドから呼ばれても安全に更新できます。

7-3. フォームを閉じた後にBeginInvokeして例外が出る場合

フォームを閉じた後にバックグラウンド処理が残っていると、BeginInvokeで例外が発生することがあります。

よくあるパターンは次のようなコードです。

C#
Task.Run(() =>
{
Thread.Sleep(5000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));
});

ユーザーが5秒以内にフォームを閉じると、フォームが破棄された後にUI更新しようとしてしまいます。

対策として、フォームを閉じるときに処理をキャンセルする設計が有効です。

C#
private CancellationTokenSource _cts = new CancellationTokenSource();

private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
if (_cts.Token.IsCancellationRequested)
{
return;
}

Thread.Sleep(50);
}

if (!this.IsDisposed && !this.Disposing)
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "完了";
}));
}
});
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_cts.Cancel();
}

フォーム終了時には、バックグラウンド処理を止めることも重要です。

7-4. UIが更新されない・反映が遅いときの確認ポイント

BeginInvokeを使っているのにUIが更新されない、または反映が遅い場合は、UIスレッドが忙しすぎる可能性があります。

たとえば、UIスレッドで重い処理を実行していると、BeginInvokeで依頼された処理が実行されません。

C#
private void button1_Click(object sender, EventArgs e)
{
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新予定";
}));

Thread.Sleep(5000); // UIスレッドを止めている
}

この場合、UIスレッドがThread.Sleepで止まっているため、画面更新が遅れます。

重い処理はUIスレッドで実行せず、Task.Runなどに分けます。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(5000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "更新完了";
}));
});
}

BeginInvokeはUIスレッドの処理キューに依頼を入れる仕組みなので、UIスレッド自体が止まっていると処理されない点に注意しましょう。

7-5. デッドロックが起きる場合の見直しポイント

デッドロックが起きる場合は、InvokeWait()Resultの使い方を見直す必要があります。

特に、UIスレッド上で非同期処理の完了を同期的に待つコードは危険です。

C#
private void button1_Click(object sender, EventArgs e)
{
var task = Task.Run(() =>
{
this.Invoke((Action)(() =>
{
label1.Text = "更新";
}));
});

task.Wait(); // デッドロックの原因になる可能性
}

このような場合は、BeginInvokeを使って待ちを減らすか、async/awaitを使ってUIスレッドをブロックしない設計にします。

C#
private async void button1_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
Thread.Sleep(1000);
});

label1.Text = "完了しました";
}

async/awaitを使うと、await後にUIスレッドへ戻ってくるため、単純なケースではBeginInvokeを書かずに済むこともあります。

8. BeginInvokeとasync/await・Taskの違い

8-1. BeginInvokeとasync/awaitは目的が違う

BeginInvokeasync/awaitは、どちらも非同期処理に関係しますが、目的が違います。

BeginInvokeは、主に「指定したスレッドで処理を実行してもらう」ために使います。Windows Formsでは、別スレッドからUIスレッドへ処理を渡す目的で使われます。

一方、async/awaitは、非同期処理を読みやすく書くための構文です。ファイル読み込み、Web API呼び出し、データベースアクセス、時間のかかる処理などを待つときに使います。

C#
private async void button1_Click(object sender, EventArgs e)
{
label1.Text = "処理中です";

await Task.Delay(1000);

label1.Text = "完了しました";
}

この例では、await後も通常はUIスレッドに戻るため、BeginInvokeを使わずにUI更新できます。

8-2. Task.RunとBeginInvokeを組み合わせる考え方

CPUを使う重い処理を別スレッドで実行したい場合は、Task.Runを使います。そして、その中からUIを更新したい場合にBeginInvokeを使います。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
// 重い処理
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50);
int progress = i;

this.BeginInvoke((Action)(() =>
{
progressBar1.Value = progress;
label1.Text = $"{progress}%";
}));
}
});
}

このように、役割を分けて考えると分かりやすいです。

Task.Runは重い処理を別スレッドで実行するためのものです。BeginInvokeはUIスレッドへUI更新を渡すためのものです。

8-3. async/await時代でもBeginInvokeが使われる場面

async/awaitが普及した現在でも、BeginInvokeが使われる場面はあります。

たとえば、外部ライブラリのコールバックがUIスレッド以外で呼ばれる場合です。

C#
private void OnReceivedMessage(string message)
{
this.BeginInvoke((Action)(() =>
{
textBox1.AppendText(message + Environment.NewLine);
}));
}

また、古いWindows Formsアプリを保守している場合や、ThreadBackgroundWorker、イベントコールバックなどを使っているコードでは、BeginInvokeが今でもよく登場します。

一方、新しくアプリを設計する場合は、全体の非同期処理はasync/awaitで書き、必要に応じてUIスレッドへの切り替えにBeginInvokeを使う、という考え方が自然です。

8-4. WPFではDispatcher.Invoke/BeginInvokeを使う

WPFでは、Windows FormsのControl.BeginInvokeではなく、Dispatcher.BeginInvokeを使います。

C#
Dispatcher.BeginInvoke(new Action(() =>
{
textBlock1.Text = "WPFで更新";
}));

同期実行したい場合はDispatcher.Invokeを使います。

C#
Dispatcher.Invoke(() =>
{
textBlock1.Text = "同期的に更新";
});

WPFでも、UI要素はUIスレッドで操作する必要があります。そのため、別スレッドからUIを更新したい場合は、Dispatcherを通してUIスレッドへ処理を渡します。

8-5. Windows FormsではControl.Invoke/BeginInvokeを使う

Windows Formsでは、Control.InvokeまたはControl.BeginInvokeを使います。

C#
this.Invoke((Action)(() =>
{
label1.Text = "Invokeで更新";
}));
C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "BeginInvokeで更新";
}));

thisの代わりに、対象コントロールを指定することもできます。

C#
label1.BeginInvoke((Action)(() =>
{
label1.Text = "LabelからBeginInvoke";
}));

ただし、フォームやコントロールのハンドルが作成されていない場合や、すでに破棄されている場合には例外が発生することがあるため注意が必要です。

9. BeginInvokeの使い分け判断基準

9-1. UI更新だけならBeginInvokeが有効なケース

バックグラウンド処理から画面にメッセージを出したい、進捗率を更新したい、ボタンを有効化したい、といった単純なUI更新であればBeginInvokeが有効です。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "処理中です";
}));

呼び出し元がUI更新の完了を待つ必要がない場合、BeginInvokeを使うことでバックグラウンド処理を止めずに済みます。

たとえば、ログ表示や進捗表示のように「表示できればよい」処理では、BeginInvokeが向いています。

9-2. 処理結果を待ちたいならInvokeを検討する

UIスレッド上で値を取得し、その結果を呼び出し元で使いたい場合は、Invokeを検討します。

C#
string input = (string)this.Invoke(new Func<string>(() =>
{
return textBox1.Text;
}));

このように、TextBoxの値を取得してバックグラウンド処理で使いたい場合は、結果を待つ必要があります。

ただし、Invokeは呼び出し元スレッドを待機させるため、デッドロックには注意が必要です。特にUIスレッドでWait()Resultを使っている場合は、設計を見直しましょう。

9-3. 非同期処理全体はasync/awaitで設計する

新しくC#アプリを作るなら、非同期処理全体はasync/awaitを中心に設計するのがおすすめです。

C#
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
label1.Text = "処理中です";

try
{
await Task.Run(() =>
{
Thread.Sleep(3000);
});

label1.Text = "完了しました";
}
finally
{
button1.Enabled = true;
}
}

この例では、await Task.Runの後にUIスレッドへ戻るため、そのままlabel1.Textbutton1.Enabledを操作できます。

そのため、すべての場面でBeginInvokeが必要になるわけではありません。

9-4. 初心者が迷いやすい選び方の基準

初心者が迷ったときは、次の基準で考えると分かりやすいです。

バックグラウンドスレッドからUIを更新したいだけなら、BeginInvokeを使います。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "更新";
}));

UIスレッドで処理した結果を呼び出し元で受け取りたいなら、Invokeを使います。

C#
string text = (string)this.Invoke(new Func<string>(() => textBox1.Text));

ボタンクリックから始まる非同期処理なら、まずはasync/awaitで書けないかを考えます。

C#
private async void button1_Click(object sender, EventArgs e)
{
await Task.Delay(1000);
label1.Text = "完了";
}

つまり、BeginInvokeは非同期処理そのものを設計するための万能ツールではなく、UIスレッドへ処理を渡すための手段として理解するのが大切です。

10. BeginInvokeに関するよくある質問

10-1. BeginInvokeは非推奨なのか

Windows FormsのControl.BeginInvokeやWPFのDispatcher.BeginInvokeは、UIスレッドへ処理を渡す目的で現在でも使われます。

ただし、C#には複数のBeginInvokeがあります。特にDelegate.BeginInvokeは古い非同期プログラミングモデルで使われていたもので、現在の新しいコードではTaskasync/awaitを使うのが一般的です。

そのため、「BeginInvokeは非推奨なのか」という質問に対しては、どのBeginInvokeを指しているかで答えが変わります。

UI更新のためのControl.BeginInvokeDispatcher.BeginInvokeは今でも使われます。一方、非同期処理全体を設計する目的では、async/awaitTaskを優先するのが基本です。

10-2. BeginInvokeとDelegate.BeginInvokeは同じなのか

同じ名前ですが、意味が異なる場合があります。

Windows Formsでよく使うBeginInvokeは、Control.BeginInvokeです。これは、コントロールを作成したスレッド、つまりUIスレッドでデリゲートを実行するためのものです。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "UI更新";
}));

一方、Delegate.BeginInvokeは、デリゲートを非同期実行する古い仕組みです。

同じBeginInvokeという名前でも、どのクラスのメソッドなのかを確認することが重要です。

初心者がWindows FormsのUI更新で調べている場合、多くはControl.BeginInvokeを指しています。

10-3. BeginInvokeで戻り値を受け取れるのか

Control.BeginInvokeIAsyncResultを返しますが、初心者が期待するように「その場ですぐ戻り値を受け取る」用途には向いていません。

戻り値をすぐ使いたい場合は、Invokeのほうが分かりやすいです。

C#
string text = (string)this.Invoke(new Func<string>(() =>
{
return textBox1.Text;
}));

BeginInvokeは、戻り値を受け取るというより、UIスレッドへ処理を依頼するために使うのが基本です。

画面更新だけならBeginInvoke、結果が必要ならInvokeまたはasync/awaitを使った設計を検討しましょう。

10-4. BeginInvokeを使えば必ずUIが固まらなくなるのか

BeginInvokeを使えば必ずUIが固まらなくなるわけではありません。

BeginInvokeは、処理をUIスレッドへ渡します。つまり、BeginInvokeの中に重い処理を書くと、その重い処理はUIスレッドで実行されます。

C#
this.BeginInvoke((Action)(() =>
{
Thread.Sleep(5000); // UIスレッドが止まる
label1.Text = "完了";
}));

このようなコードでは、画面が固まる可能性があります。

UIを固めないためには、重い処理をTask.Runなどでバックグラウンドに移し、UI更新だけをBeginInvokeで行うことが大切です。

C#
Task.Run(() =>
{
Thread.Sleep(5000);

this.BeginInvoke((Action)(() =>
{
label1.Text = "完了";
}));
});

BeginInvokeは、画面フリーズを防ぐ部品のひとつですが、設計全体も重要です。

10-5. WPFでもWindows Formsと同じ書き方で使えるのか

WPFでは、Windows Formsと同じControl.BeginInvokeの書き方は使いません。

Windows Formsでは次のように書きます。

C#
this.BeginInvoke((Action)(() =>
{
label1.Text = "Windows Forms";
}));

WPFではDispatcher.BeginInvokeを使います。

C#
Dispatcher.BeginInvoke(new Action(() =>
{
textBlock1.Text = "WPF";
}));

考え方は同じです。別スレッドからUIを直接操作せず、UIスレッドへ処理を渡します。

ただし、使うクラスや書き方はフレームワークによって異なります。Windows FormsならControl.InvokeControl.BeginInvoke、WPFならDispatcher.InvokeDispatcher.BeginInvokeと覚えておきましょう。

まとめ

C#のBeginInvokeは、主に別スレッドからUIスレッドへ処理を渡すために使われるメソッドです。

特にWindows Formsでは、バックグラウンド処理からLabelTextBoxなどのコントロールを直接操作すると、クロススレッド操作の例外が発生することがあります。そのような場合に、Control.BeginInvokeを使ってUIスレッド上で安全に更新します。

InvokeBeginInvokeの違いは、同期実行か非同期実行かです。Invokeは処理完了を待ち、BeginInvokeは処理完了を待たずに呼び出し元が次へ進みます。

UI更新だけを依頼したい場合はBeginInvokeが便利です。一方、UIスレッド上で値を取得して結果を待ちたい場合はInvokeを検討します。

また、BeginInvokeを使えば必ず画面が固まらなくなるわけではありません。重い処理はTask.Runasync/awaitで適切に分離し、BeginInvokeの中には短いUI更新処理だけを書くことが大切です。

初心者のうちは、次のように覚えておくとよいでしょう。

C#
this.BeginInvoke((Action)(() =>
{
// UIスレッドで実行したい画面更新
}));

BeginInvokeは、C#のUIアプリでスレッドセーフに画面を更新するための重要な仕組みです。Invokeasync/awaitとの違いを理解し、場面に応じて正しく使い分けましょう。