C# BackgroundWorkerの使い方を初心者向けに解説|UIが固まらない非同期処理・進捗表示・キャンセル対応まで

はじめに

C#でWindows Formsアプリを作っていると、「ボタンを押した瞬間に画面が固まる」「ProgressBarが更新されない」「キャンセルボタンを押しても反応しない」といった問題に出会うことがあります。

この原因の多くは、時間のかかる処理をUIスレッドで直接実行していることです。UIスレッドは、ボタンのクリック、画面の再描画、ラベルの更新などを担当しています。そのため、同じスレッドで重い処理を実行すると、画面操作や再描画が止まったように見えてしまいます。

そこで使えるのがBackgroundWorkerです。BackgroundWorkerを使うと、時間のかかる処理をUIスレッドとは別のスレッドで実行し、処理中の進捗表示や完了後のUI更新、キャンセル処理を比較的わかりやすく実装できます。Microsoft Learnでも、BackgroundWorkerは時間のかかる処理を別スレッドで実行し、UIの応答性を保つためのクラスとして説明されています。

この記事では、C#のBackgroundWorkerの基本から、非同期処理、進捗表示、キャンセル、引数と戻り値、例外処理、よくあるエラー、実践的なサンプルコードまで初心者向けに解説します。

1. C#のBackgroundWorkerとは?UIが固まる原因と解決できること

1-1. BackgroundWorkerの役割を初心者向けにわかりやすく解説

BackgroundWorkerは、C#でバックグラウンド処理を実行するためのクラスです。主にWindows Formsアプリで使われ、時間のかかる処理を画面とは別のスレッドで動かすために利用されます。

たとえば、次のような処理は時間がかかることがあります。

  • 大量データの読み込み

  • ファイルのコピー

  • 画像変換

  • CSVやExcelファイルの解析

  • データベース処理

  • 外部APIとの通信

  • 重い計算処理

これらをボタンクリックイベントの中で直接実行すると、処理が終わるまで画面が反応しなくなる場合があります。BackgroundWorkerを使うと、重い処理をDoWorkイベントで実行し、画面更新はProgressChangedRunWorkerCompletedで行う、という役割分担ができます。

1-2. UIスレッドで重い処理を実行すると画面が固まる理由

Windows Formsの画面は、基本的にUIスレッドと呼ばれる1つのスレッドで操作されます。このUIスレッドは、次のような処理を担当しています。

UIスレッドの役割具体例
ユーザー操作の受付ボタンクリック、テキスト入力
画面の描画ラベル更新、ProgressBar更新
イベント処理Click、Load、TextChangedなど
フォームの応答移動、リサイズ、閉じる操作

このUIスレッド上でforループやThread.Sleep、ファイル処理などの重い処理を実行すると、その間は画面の再描画やクリックイベントを処理できません。その結果、ユーザーから見ると「アプリが固まった」「応答なしになった」と感じます。

悪い例は次のようなコードです。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中...";

for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000); // 重い処理の代わり
}

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

このコードでは、buttonStart_ClickがUIスレッド上で実行されます。Thread.Sleepで止まっている間、画面は更新されず、ボタンも反応しにくくなります。

1-3. BackgroundWorkerでできること:非同期処理・進捗表示・完了通知・キャンセル

BackgroundWorkerでは、主に次のことができます。

機能内容
非同期処理重い処理をUIスレッドとは別に実行する
進捗表示ReportProgressで進捗率を通知する
UI更新ProgressChangedRunWorkerCompletedで安全に画面を更新する
完了通知処理が終わったタイミングで結果を受け取る
キャンセルCancelAsyncでキャンセル要求を出す
エラー処理RunWorkerCompletedで例外を確認する

BackgroundWorkerでは、バックグラウンド処理を開始するためにRunWorkerAsyncを呼び出し、処理完了時にはRunWorkerCompletedイベントが発生します。公式ドキュメントでも、この流れが基本的な使い方として説明されています。

1-4. WinFormsアプリでBackgroundWorkerが使われる代表的な場面

BackgroundWorkerは、特にWindows Formsアプリで次のような場面に向いています。

場面使用例
ファイル処理CSV読み込み、ログ解析、ファイルコピー
データ取得DB検索、API通信、外部ファイル読み込み
画像処理リサイズ、変換、サムネイル生成
一括処理複数ファイルの変換、一括登録
レガシーアプリ保守既存WinFormsアプリの改善
進捗表示ProgressBarで処理状況を表示
キャンセル処理長時間処理を途中で止める

初心者にとっては、DoWorkProgressChangedRunWorkerCompletedという3つのイベントに処理を分けられるため、非同期処理の流れを理解しやすい点がメリットです。

1-5. Task/async-awaitとの違いと、今でもBackgroundWorkerを使うケース

現在のC#では、非同期処理にはTaskasync-awaitを使うことが一般的です。特に、Web API呼び出し、ファイル読み込み、データベースアクセスなどのI/O処理ではasync-awaitの方が自然に書けます。

一方、BackgroundWorkerはイベントベースで非同期処理を扱う古くからある仕組みです。新規開発ではTaskasync-awaitを検討することが多いですが、次のようなケースでは今でもBackgroundWorkerが使われます。

BackgroundWorkerを使うケース理由
既存のWinFormsアプリを保守しているすでに実装されていることが多い
デザイナーから追加したいVisual Studioのコンポーネントとして扱いやすい
進捗・完了・キャンセルをイベントで分けたい初心者にも構造が見えやすい
簡単なバックグラウンド処理を追加したい小規模な処理なら実装しやすい
.NET Framework時代のアプリを扱うレガシー環境で使いやすい

Microsoft Learnでは、BackgroundWorkerは.NET 10.0向けAPIとしても掲載されており、Windows Formsのバックグラウンド処理の説明にも使われています。ただし、現代的なC#の非同期処理ではasync-awaitも重要な選択肢です。

2. BackgroundWorkerを使う前に押さえる基本構成

2-1. BackgroundWorkerの主要プロパティ一覧

BackgroundWorkerを使う前に、まず主要なプロパティを押さえておきましょう。

プロパティ内容
WorkerReportsProgressbool進捗報告を使うかどうか
WorkerSupportsCancellationboolキャンセルを使うかどうか
CancellationPendingboolキャンセル要求が出ているか
IsBusybool現在実行中かどうか

特に重要なのは、WorkerReportsProgressWorkerSupportsCancellationです。進捗表示を使うならWorkerReportsProgress = true、キャンセルを使うならWorkerSupportsCancellation = trueを設定します。

C#
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;

WorkerReportsProgressfalseのままReportProgressを呼ぶと例外が発生します。公式ドキュメントでも、ReportProgressを使うにはWorkerReportsProgresstrueにする必要があると説明されています。

2-2. DoWork・ProgressChanged・RunWorkerCompletedの役割

BackgroundWorkerで特に重要なのは、次の3つのイベントです。

イベント実行されるタイミング主な役割
DoWorkバックグラウンド処理開始時重い処理を書く
ProgressChanged進捗通知時ProgressBarやLabelを更新する
RunWorkerCompleted処理完了時完了後のUI更新、結果表示、エラー処理

役割を簡単に分けると、次のようになります。

DoWork
→ 重い処理を書く場所

ProgressChanged
→ 処理中の進捗を画面に表示する場所

RunWorkerCompleted
→ 処理完了後に画面を元に戻す場所

重要なのは、DoWorkではUIコントロールを直接操作しないことです。DoWorkはUIスレッドとは別のスレッドで実行されるため、labelStatus.Text = "処理中";のような画面操作を直接書くとエラーの原因になります。

2-3. RunWorkerAsyncでバックグラウンド処理を開始する流れ

BackgroundWorkerの処理は、RunWorkerAsyncを呼び出すことで開始します。

C#
backgroundWorker1.RunWorkerAsync();

基本的な流れは次のとおりです。

1. ボタンをクリックする
2. RunWorkerAsyncを呼ぶ
3. DoWorkイベントが実行される
4. 必要に応じてReportProgressで進捗を通知する
5. ProgressChangedで画面を更新する
6. 処理が終わる
7. RunWorkerCompletedで完了後の画面を更新する

引数を渡したい場合は、RunWorkerAsyncに値を指定します。

C#
backgroundWorker1.RunWorkerAsync(100);

渡した引数は、DoWorkイベント内でe.Argumentから取得できます。

C#
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;
}

2-4. WorkerReportsProgressとWorkerSupportsCancellationの設定

進捗表示とキャンセル処理を使う場合は、初期設定で次のように書きます。

C#
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;

それぞれの意味は次のとおりです。

設定trueにするとできること
WorkerReportsProgressReportProgressで進捗を通知できる
WorkerSupportsCancellationCancelAsyncでキャンセル要求を出せる

設定し忘れると、進捗表示が呼ばれなかったり、キャンセルできなかったりします。

2-5. UIを直接更新してはいけない理由

Windows Formsのコントロールは、基本的に作成されたUIスレッドから操作する必要があります。DoWorkは別スレッドで実行されるため、次のようなコードは避けます。

C#
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// NG:DoWork内でUIを直接更新している
labelStatus.Text = "処理中...";
}

正しくは、ReportProgressでUIスレッド側に通知し、ProgressChangedで画面を更新します。

C#
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = (BackgroundWorker)sender;
worker.ReportProgress(50, "半分完了しました");
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
labelStatus.Text = e.UserState?.ToString();
}

ReportProgressによって発生するProgressChangedイベントは、BackgroundWorkerを作成した側のスレッドで実行されるため、UI更新に使いやすい仕組みになっています。

3. C# BackgroundWorkerの基本的な使い方

3-1. BackgroundWorkerをフォームに追加する方法

Visual StudioのWindows Formsデザイナーを使っている場合、BackgroundWorkerはツールボックスからフォームに追加できます。

手順は次のとおりです。

  1. Windows Formsのフォームを開く

  2. ツールボックスを表示する

  3. 「コンポーネント」からBackgroundWorkerを探す

  4. フォームにドラッグ&ドロップする

  5. 画面下部のコンポーネントトレイにbackgroundWorker1が表示される

  6. プロパティからWorkerReportsProgressWorkerSupportsCancellationを設定する

  7. イベント一覧からDoWorkなどを追加する

デザイナーで追加すると、自動的にbackgroundWorker1という名前のコンポーネントが作成されます。

3-2. コードでBackgroundWorkerを生成する方法

デザイナーを使わず、コードだけで作成することもできます。

C#
using System.ComponentModel;

private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;

worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}

イベントハンドラーは次のように用意します。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
}

3-3. DoWorkイベントに重い処理を書く

DoWorkには、時間のかかる処理を書きます。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(500); // 重い処理の代わり
}
}

実際のアプリでは、Thread.Sleepの部分がファイル処理、データベース処理、計算処理などになります。

3-4. ボタンクリックでRunWorkerAsyncを呼び出す

開始ボタンを押したときに、RunWorkerAsyncを呼び出します。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
worker.RunWorkerAsync();
}

ただし、すでに実行中の状態で再度RunWorkerAsyncを呼ぶとエラーになります。そのため、通常はIsBusyを確認します。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
worker.RunWorkerAsync();
}
}

3-5. RunWorkerCompletedで処理完了後のUIを更新する

処理が終わった後の画面更新は、RunWorkerCompletedに書きます。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
labelStatus.Text = "完了しました";
buttonStart.Enabled = true;
}

RunWorkerCompletedでは、成功、キャンセル、エラーを分けて処理できます。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
}
else
{
labelStatus.Text = "完了しました";
}

buttonStart.Enabled = true;
}

3-6. 最小構成のサンプルコード

最小限のBackgroundWorkerサンプルは次のとおりです。

C#
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}

private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}

labelStatus.Text = "処理中...";
buttonStart.Enabled = false;

worker.RunWorkerAsync();
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
// 重い処理の代わり
Thread.Sleep(3000);
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
labelStatus.Text = "完了しました";
}
}

このコードでは、ボタンを押すと3秒間の処理がバックグラウンドで実行されます。その間もUIスレッドは止まらないため、画面が固まりにくくなります。

4. UIが固まらない非同期処理の実装例

4-1. 悪い例:ボタンクリック内で重い処理を直接実行するコード

まず、UIが固まる悪い例を見てみましょう。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中...";
progressBar1.Value = 0;

for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
progressBar1.Value = i * 10;
}

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

このコードは一見問題なさそうに見えます。しかし、buttonStart_Clickの中で重い処理を直接実行しているため、画面が固まる原因になります。

特に次のような問題が起こります。

問題内容
画面が固まる処理中にフォームを移動できない
ProgressBarが更新されない処理後に一気に100%になる
キャンセルできないボタン操作を受け付けにくい
応答なしになるWindowsから停止したように見える

4-2. 良い例:BackgroundWorkerで重い処理を別スレッドに分離するコード

BackgroundWorkerを使うと、重い処理をDoWorkに移せます。

C#
private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}

private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}

labelStatus.Text = "処理中...";
buttonStart.Enabled = false;

worker.RunWorkerAsync();
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
}
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
labelStatus.Text = "完了しました";
buttonStart.Enabled = true;
}

このようにすると、重い処理はバックグラウンドで動き、UIスレッドは画面操作を受け付けられます。

4-3. 処理中にボタンを無効化して二重実行を防ぐ

バックグラウンド処理中に開始ボタンを何度も押されると、二重実行の原因になります。そのため、処理開始時にボタンを無効化します。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}

buttonStart.Enabled = false;
labelStatus.Text = "処理中...";

worker.RunWorkerAsync();
}

処理完了後にボタンを有効化します。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
labelStatus.Text = "完了しました";
}

4-4. IsBusyで実行中かどうかを判定する

IsBusyは、BackgroundWorkerが実行中かどうかを示します。

C#
if (worker.IsBusy)
{
return;
}

開始処理では、基本的にIsBusyを確認してからRunWorkerAsyncを呼び出します。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
worker.RunWorkerAsync();
}
}

これにより、実行中のBackgroundWorkerに対して再度RunWorkerAsyncを呼んでしまうミスを防げます。

4-5. 処理完了後にボタンやラベルを元に戻す

処理中に変更したUIは、完了時に必ず元に戻します。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
}
else
{
labelStatus.Text = "完了しました";
}
}

完了時にUIを復旧する処理をまとめておくと、通常完了、キャンセル、エラーのいずれの場合でも画面が中途半端な状態になりにくくなります。

5. ProgressBarで進捗表示を行う方法

5-1. ReportProgressで進捗率を通知する

進捗表示を行うには、まずWorkerReportsProgresstrueにします。

C#
worker.WorkerReportsProgress = true;

次に、DoWork内でReportProgressを呼び出します。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;

for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
bw.ReportProgress(i);
}
}

ReportProgressを呼ぶと、ProgressChangedイベントが発生します。進捗率はe.ProgressPercentageで受け取れます。

5-2. ProgressChangedでProgressBarとLabelを更新する

ProgressBarやLabelの更新はProgressChangedで行います。

C#
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = $"{e.ProgressPercentage}% 完了";
}

ProgressChangedはUI更新用のイベントとして使えるため、ProgressBarLabelの更新を書く場所として適しています。

5-3. 0〜100%の進捗率を計算する考え方

ReportProgressに渡す値は、通常0〜100の整数で表します。

たとえば、100件のデータを処理する場合は次のように計算します。

C#
int percent = (i + 1) * 100 / totalCount;
worker.ReportProgress(percent);

全体件数がtotalCount、現在処理した件数がi + 1なら、進捗率は次の式で求められます。

進捗率 = 処理済み件数 × 100 / 全体件数

コード例は次のとおりです。

C#
int totalCount = 50;

for (int i = 0; i < totalCount; i++)
{
Thread.Sleep(100);

int percent = (i + 1) * 100 / totalCount;
worker.ReportProgress(percent);
}

5-4. UserStateを使ってメッセージや処理状況を渡す

ReportProgressには、進捗率だけでなく任意のオブジェクトも渡せます。

C#
worker.ReportProgress(percent, "ファイルを読み込み中...");

この第2引数は、ProgressChanged側でe.UserStateとして受け取れます。

C#
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}

UserStateを使うと、次のような情報をUIに渡せます。

渡す情報
メッセージ"読み込み中..."
ファイル名"data.csv"
現在件数"10 / 100 件"
独自クラス処理状況DTO

ReportProgress(Int32, Object)では、進捗率に加えて任意の状態情報を渡せます。公式ドキュメントでも、userStateProgressChangedEventArgs.UserStateとして返される値として説明されています。

5-5. 進捗表示付きのサンプルコード

進捗表示付きの基本サンプルです。

C#
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.WorkerReportsProgress = true;

worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}

private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}

progressBar1.Value = 0;
labelStatus.Text = "開始しました";
buttonStart.Enabled = false;

worker.RunWorkerAsync();
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;

int total = 20;

for (int i = 0; i < total; i++)
{
Thread.Sleep(200);

int percent = (i + 1) * 100 / total;
bw.ReportProgress(percent, $"{i + 1} / {total} 件を処理しました");
}
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
labelStatus.Text = "完了しました";
}
}

このコードでは、処理件数に応じてProgressBarとLabelが更新されます。

5-6. 進捗が不明な処理でマーキースタイルを使う方法

処理の全体件数がわからない場合、0〜100%の進捗率を計算できません。たとえば、外部APIの応答待ちや、終了時間が予測できない処理では進捗率が不明なことがあります。

その場合は、ProgressBarのStyleMarqueeにします。

C#
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.MarqueeAnimationSpeed = 30;

処理完了後は通常表示に戻します。

C#
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Value = 0;

使用例です。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}

progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.MarqueeAnimationSpeed = 30;
labelStatus.Text = "処理中...";

worker.RunWorkerAsync();
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Value = 0;

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

6. キャンセル処理を実装する方法

6-1. CancelAsyncでキャンセル要求を出す

BackgroundWorkerでキャンセル処理を使うには、まずWorkerSupportsCancellationtrueにします。

C#
worker.WorkerSupportsCancellation = true;

キャンセルボタンではCancelAsyncを呼び出します。

C#
private void buttonCancel_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
worker.CancelAsync();
labelStatus.Text = "キャンセル要求中...";
}
}

ここで注意したいのは、CancelAsyncを呼んだからといって即座に処理が強制停止されるわけではないことです。CancelAsyncは「キャンセルしてほしい」という要求を出すだけです。

6-2. CancellationPendingでキャンセル要求を確認する

実際に処理を止めるには、DoWork内でCancellationPendingを定期的に確認します。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;

for (int i = 0; i < 100; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}

Thread.Sleep(100);
}
}

長いループ処理では、毎回または一定間隔でCancellationPendingを確認するのが基本です。

6-3. e.Cancelをtrueにしてキャンセル完了を通知する

キャンセル要求を検知したら、e.Cancel = true;を設定してDoWorkを抜けます。

C#
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}

これにより、RunWorkerCompletede.Cancelledtrueになります。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
}

6-4. RunWorkerCompletedで通常完了・キャンセル・エラーを分岐する

RunWorkerCompletedでは、次の順番で確認するのが安全です。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(e.Error.Message);
}
else
{
labelStatus.Text = "正常に完了しました";
}

buttonStart.Enabled = true;
buttonCancel.Enabled = false;
}

Resultを使う場合は、ErrorCancelledを確認してからアクセスする必要があります。公式ドキュメントでも、Errorがある場合やキャンセル済みの場合にResultへアクセスすると例外につながるため、先にErrorCancelledを確認するよう説明されています。

6-5. キャンセルボタン付きのサンプルコード

C#
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;

worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;

buttonCancel.Enabled = false;
}

private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}

progressBar1.Value = 0;
labelStatus.Text = "処理中...";
buttonStart.Enabled = false;
buttonCancel.Enabled = true;

worker.RunWorkerAsync();
}

private void buttonCancel_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
worker.CancelAsync();
labelStatus.Text = "キャンセル要求中...";
buttonCancel.Enabled = false;
}
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;

int total = 50;

for (int i = 0; i < total; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}

Thread.Sleep(100);

int percent = (i + 1) * 100 / total;
bw.ReportProgress(percent, $"{i + 1} / {total} 件を処理中");
}
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(e.Error.Message);
}
else
{
progressBar1.Value = 100;
labelStatus.Text = "完了しました";
}
}
}

6-6. キャンセルできない実装になってしまう原因

キャンセルボタンを押しても止まらない場合、よくある原因は次のとおりです。

原因解決策
WorkerSupportsCancellationfalsetrueに設定する
CancelAsyncを呼んでいないキャンセルボタンで呼び出す
CancellationPendingを確認していないDoWork内で定期的に確認する
e.Cancel = trueを設定していないキャンセル検知時に設定する
1回の処理が長すぎる処理を分割し、途中で確認できるようにする
外部処理がブロックしているタイムアウトや別のキャンセル機構を検討する

BackgroundWorkerのキャンセルは強制終了ではありません。処理側がキャンセル要求を見に行き、自分で終了する必要があります。

7. BackgroundWorkerで引数や戻り値を扱う方法

7-1. RunWorkerAsyncの引数をDoWorkで受け取る

BackgroundWorkerでは、RunWorkerAsyncに引数を渡せます。

C#
worker.RunWorkerAsync(10);

この値は、DoWorkイベントで受け取ります。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;
}

7-2. DoWorkEventArgs.Argumentの使い方

e.Argumentobject型なので、受け取るときはキャストが必要です。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
string filePath = (string)e.Argument;

// filePathを使って処理する
}

複数の値を渡す場合は、クラスやタプルを使うと便利です。

7-3. e.Resultで処理結果を返す

DoWork内で処理結果を返したい場合は、e.Resultに値を設定します。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;

int result = count * 2;

e.Result = result;
}

7-4. RunWorkerCompletedEventArgs.Resultで結果を取得する

e.Resultに設定した値は、RunWorkerCompletedで取得できます。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
return;
}

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}

int result = (int)e.Result;
labelStatus.Text = $"結果:{result}";
}

ここでも、Resultへアクセスする前にErrorCancelledを確認することが重要です。

7-5. 複数の値を渡す場合のクラス・タプル・DTOの使い分け

複数の値を渡す方法はいくつかあります。

方法向いているケース
クラスSearchCondition意味のあるデータをまとめる
タプル(string path, int count)小規模な一時データ
DTOImportRequestDto実務アプリで入力条件を明確にする

初心者には、まずクラスを作る方法がおすすめです。値の意味がわかりやすく、後から項目を追加しやすいためです。

C#
public class SearchRequest
{
public string Keyword { get; set; }
public int MaxCount { get; set; }
}

戻り値もクラスにできます。

C#
public class SearchResult
{
public int Count { get; set; }
public string Message { get; set; }
}

7-6. 引数と戻り値を使った実用サンプルコード

C#
public class ProcessRequest
{
public int Count { get; set; }
public string Name { get; set; }
}

public class ProcessResult
{
public int Total { get; set; }
public string Message { get; set; }
}
C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}

var request = new ProcessRequest
{
Count = 10,
Name = "サンプル処理"
};

worker.RunWorkerAsync(request);
}
C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var request = (ProcessRequest)e.Argument;

int total = 0;

for (int i = 1; i <= request.Count; i++)
{
Thread.Sleep(100);
total += i;
}

e.Result = new ProcessResult
{
Total = total,
Message = $"{request.Name}が完了しました"
};
}
C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
return;
}

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}

var result = (ProcessResult)e.Result;

labelStatus.Text = $"{result.Message} 合計:{result.Total}";
}

8. 例外処理とエラー表示の実装

8-1. DoWork内で例外が発生した場合の動き

DoWork内で例外が発生した場合、その例外はRunWorkerCompletede.Errorで確認できます。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
throw new InvalidOperationException("サンプルエラーです");
}

この例外は、RunWorkerCompletedで次のように受け取ります。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
}
}

8-2. RunWorkerCompletedのErrorプロパティを確認する

エラー処理では、まずe.Errorを確認します。

C#
if (e.Error != null)
{
MessageBox.Show($"エラーが発生しました:{e.Error.Message}");
return;
}

その後にe.Cancelled、最後に正常完了を処理すると安全です。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show($"エラー:{e.Error.Message}");
}
else if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else
{
labelStatus.Text = "正常に完了しました";
}
}

8-3. MessageBoxでエラー内容を表示する

初心者向けの実装では、まずMessageBox.Showでエラー内容を表示するとわかりやすいです。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";

MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);

return;
}

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

実務では、ログファイルにエラー内容を出力する処理も追加するとよいでしょう。

8-4. finally相当の後片付けをどこに書くべきか

BackgroundWorkerでは、後片付けの内容によって書く場所を分けます。

後片付けの内容書く場所
ファイルやDB接続の解放DoWork内のfinallyまたはusing
ボタンを有効化するRunWorkerCompleted
ProgressBarを戻すRunWorkerCompleted
一時ファイル削除内容によってDoWorkまたはRunWorkerCompleted

UIに関係する後片付けはRunWorkerCompletedに書きます。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
progressBar1.Style = ProgressBarStyle.Blocks;
}

ファイルやリソースの解放はDoWork内で行います。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
using (var stream = File.OpenRead("sample.txt"))
{
// ファイル処理
}
}

8-5. エラー時にUIを安全に復旧するコード例

エラーが発生してもボタンが無効のままにならないように、UI復旧処理は最後にまとめます。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
progressBar1.Style = ProgressBarStyle.Blocks;

if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";

MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);

return;
}

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}

progressBar1.Value = 100;
labelStatus.Text = "完了しました";
}

このように、ボタンやProgressBarを元に戻す処理を最初に書くと、エラー時でも画面が復旧しやすくなります。

9. BackgroundWorkerでよくあるエラーと解決策

9-1. 「別スレッドから呼び出されたため、コントロールにアクセスできません」の原因

このエラーは、DoWork内でUIコントロールを直接操作したときによく発生します。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
// NG
labelStatus.Text = "処理中...";
}

解決策は、ReportProgressを使ってProgressChangedでUIを更新することです。

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
bw.ReportProgress(0, "処理中...");
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
labelStatus.Text = e.UserState?.ToString();
}

9-2. ProgressChangedが呼ばれない原因

ProgressChangedが呼ばれない場合は、次を確認します。

確認項目内容
WorkerReportsProgresstrueになっているか
イベント登録ProgressChanged += ...しているか
ReportProgressDoWork内で呼んでいるか
例外発生DoWork内で途中停止していないか

よくあるミスは、WorkerReportsProgressの設定忘れです。

C#
worker.WorkerReportsProgress = true;

9-3. キャンセルボタンを押しても止まらない原因

キャンセルできない場合は、次を確認します。

C#
worker.WorkerSupportsCancellation = true;
C#
worker.CancelAsync();
C#
if (worker.CancellationPending)
{
e.Cancel = true;
return;
}

CancelAsyncだけでは処理は止まりません。DoWork内でCancellationPendingを確認し、e.Cancel = trueを設定して処理を抜ける必要があります。

9-4. RunWorkerAsyncを連続実行してエラーになる原因

BackgroundWorkerは、実行中に再度RunWorkerAsyncを呼ぶとエラーになります。対策としてIsBusyを確認します。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}

worker.RunWorkerAsync();
}

また、処理中は開始ボタンを無効化しておくと、二重実行を防ぎやすくなります。

C#
buttonStart.Enabled = false;

9-5. 処理完了後にResultを取得すると例外になる原因

RunWorkerCompletede.Resultにアクセスする前に、e.Errore.Cancelledを確認していない場合、例外が発生することがあります。

悪い例です。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// NG
var result = e.Result;
}

良い例です。

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
return;
}

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}

var result = e.Result;
}

RunWorkerCompletedEventArgs.Resultは、エラーやキャンセルの状態によって例外を発生させる可能性があるため、先に状態確認を行うのが基本です。

9-6. デバッグ時に確認すべきチェックリスト

BackgroundWorkerがうまく動かないときは、次のチェックリストを確認してください。

チェック項目確認内容
DoWorkが呼ばれているかブレークポイントで確認
RunWorkerAsyncを呼んでいるかボタンクリック処理を確認
IsBusyで弾かれていないか条件分岐を確認
WorkerReportsProgresstrue進捗表示に必要
WorkerSupportsCancellationtrueキャンセルに必要
UIをDoWorkで直接触っていないかクロススレッドエラーの原因
ReportProgressを呼んでいるかProgressChanged発火に必要
e.Cancel = trueを設定しているかキャンセル完了判定に必要
e.Resultの前にErrorCancelledを確認しているか例外防止
イベント登録が重複していないか二重実行や重複表示の原因

10. 実践サンプル:時間のかかる処理を進捗表示・キャンセル対応で実装する

10-1. 作成するサンプルアプリの概要

ここでは、次の機能を持つWindows Formsアプリを作成します。

機能内容
開始ボタンバックグラウンド処理を開始する
キャンセルボタン処理中にキャンセル要求を出す
ProgressBar進捗率を表示する
Label現在の処理状況を表示する
完了処理正常完了、キャンセル、エラーを分岐する

処理内容は、100件のデータを順番に処理する想定にします。

10-2. 画面に配置するコントロール一覧

フォームには次のコントロールを配置します。

コントロールName用途
ButtonbuttonStart処理開始
ButtonbuttonCancelキャンセル
ProgressBarprogressBar1進捗表示
LabellabelStatus状態表示

初期状態では、キャンセルボタンを無効にしておきます。

C#
buttonCancel.Enabled = false;

10-3. BackgroundWorkerの初期設定コード

C#
private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;

worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;

buttonCancel.Enabled = false;
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
labelStatus.Text = "待機中";
}

10-4. 開始ボタンの処理

C#
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}

progressBar1.Value = 0;
labelStatus.Text = "処理を開始しました";

buttonStart.Enabled = false;
buttonCancel.Enabled = true;

worker.RunWorkerAsync(100);
}

ここでは、RunWorkerAsync(100)で処理件数を渡しています。

10-5. DoWork内の重い処理

C#
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;

int totalCount = (int)e.Argument;
int processedCount = 0;

for (int i = 0; i < totalCount; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}

// 重い処理の代わり
Thread.Sleep(50);

processedCount++;

int percent = processedCount * 100 / totalCount;
string message = $"{processedCount} / {totalCount} 件を処理しました";

bw.ReportProgress(percent, message);
}

e.Result = processedCount;
}

10-6. 進捗表示の更新処理

C#
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}

ProgressChangedではUIコントロールを安全に更新できます。

10-7. キャンセルボタンの処理

C#
private void buttonCancel_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
return;
}

worker.CancelAsync();

buttonCancel.Enabled = false;
labelStatus.Text = "キャンセル要求中...";
}

CancelAsyncはキャンセル要求を出すだけなので、実際の停止はDoWork内のCancellationPendingチェックで行います。

10-8. 完了・キャンセル・エラー時の表示切り替え

C#
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;

if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";

MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);

return;
}

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}

int processedCount = (int)e.Result;

progressBar1.Value = 100;
labelStatus.Text = $"{processedCount} 件の処理が完了しました";
}

10-9. サンプルコード全文

C#
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();

public Form1()
{
InitializeComponent();

worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;

worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;

buttonCancel.Enabled = false;

progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;

labelStatus.Text = "待機中";
}

private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}

progressBar1.Value = 0;
labelStatus.Text = "処理を開始しました";

buttonStart.Enabled = false;
buttonCancel.Enabled = true;

// 100件のデータを処理する想定
worker.RunWorkerAsync(100);
}

private void buttonCancel_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
return;
}

worker.CancelAsync();

buttonCancel.Enabled = false;
labelStatus.Text = "キャンセル要求中...";
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;

int totalCount = (int)e.Argument;
int processedCount = 0;

for (int i = 0; i < totalCount; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}

// 実際のアプリでは、ここにファイル処理や計算処理を書く
Thread.Sleep(50);

processedCount++;

int percent = processedCount * 100 / totalCount;
string message = $"{processedCount} / {totalCount} 件を処理しました";

bw.ReportProgress(percent, message);
}

e.Result = processedCount;
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;

if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";

MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);

return;
}

if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}

int processedCount = (int)e.Result;

progressBar1.Value = 100;
labelStatus.Text = $"{processedCount} 件の処理が完了しました";
}
}

このサンプルでは、BackgroundWorkerの基本である非同期処理、進捗表示、キャンセル、完了後のUI更新、エラー処理をまとめて実装しています。

11. BackgroundWorkerを使うときの注意点とベストプラクティス

11-1. DoWork内でUIコントロールを直接操作しない

最も重要な注意点は、DoWork内でUIコントロールを直接操作しないことです。

C#
// NG
labelStatus.Text = "処理中";

UIを更新したい場合は、ReportProgressを使います。

C#
bw.ReportProgress(percent, "処理中");

そして、ProgressChangedでUIを更新します。

C#
labelStatus.Text = e.UserState?.ToString();

11-2. 長時間ループでは定期的にCancellationPendingを確認する

キャンセル対応を入れる場合は、長時間ループの中でCancellationPendingを定期的に確認します。

C#
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}

確認頻度が低すぎると、キャンセルボタンを押してもなかなか止まりません。

11-3. 重い処理とUI更新の責務を分ける

BackgroundWorkerを使うときは、次のように責務を分けると読みやすくなります。

場所書く処理
DoWork重い処理、計算、ファイル処理
ProgressChangedProgressBar、Label更新
RunWorkerCompleted完了後の画面復旧、結果表示、エラー表示

この分け方を守ると、コードの見通しがよくなります。

11-4. ProgressChangedの呼び出し頻度を上げすぎない

ReportProgressを呼びすぎると、UI更新が多くなりすぎて逆に重くなることがあります。

たとえば、100万件のループで毎回ReportProgressを呼ぶのは避けた方がよいです。

C#
// あまりよくない例
for (int i = 0; i < 1000000; i++)
{
bw.ReportProgress(i);
}

一定間隔で更新するようにします。

C#
for (int i = 0; i < total; i++)
{
if (i % 100 == 0)
{
int percent = i * 100 / total;
bw.ReportProgress(percent);
}
}

11-5. 複雑な非同期処理ではTaskやasync-awaitも検討する

BackgroundWorkerはわかりやすい一方で、複雑な非同期処理には向かない場合があります。

次のようなケースでは、Taskasync-awaitも検討しましょう。

ケース検討したい方法
Web APIを呼び出すasync-await
ファイルを非同期に読み書きするasync-await
複数処理を並列実行するTask
キャンセルトークンを使いたいCancellationToken
新規の.NETアプリを作るasync-await

C#の非同期プログラミングでは、Taskを中心としたasync-await構文が広く使われています。Microsoft Learnでも、async-awaitは非同期処理を順序立ったコードのように書ける仕組みとして説明されています。

11-6. 新規開発でBackgroundWorkerを選ぶべきかの判断基準

新規開発でBackgroundWorkerを使うかどうかは、次の基準で判断するとよいです。

判断基準おすすめ
既存WinFormsアプリの改修BackgroundWorkerでもよい
デザイナー中心で作りたいBackgroundWorkerでもよい
初心者が進捗・キャンセルを学びたいBackgroundWorkerは理解しやすい
新規のモダンなC#アプリTask / async-awaitを検討
複雑な非同期制御が必要Task / async-awaitを検討
WPFやMAUIなども視野に入れるasync-awaitを検討

結論として、BackgroundWorkerは古い技術ではありますが、WinFormsアプリでは今でも理解しておく価値があります。特に既存アプリの保守や、UIが固まる問題の改善では役立ちます。

12. C# BackgroundWorkerに関するよくある質問

12-1. BackgroundWorkerは非推奨ですか?

BackgroundWorkerは古くからある仕組みですが、公式APIとして現在の.NET向けドキュメントにも掲載されています。つまり、すぐに使えない技術というわけではありません。

ただし、新規開発ではTaskasync-awaitを使う設計が一般的です。特に非同期I/Oや複雑なキャンセル制御を扱う場合は、async-awaitの方が適していることが多いです。

12-2. WPFでもBackgroundWorkerは使えますか?

WPFでもBackgroundWorkerは使用できます。System.ComponentModel.BackgroundWorkerはWindows Forms専用というわけではありません。

ただし、WPFではDispatcherasync-awaitを使ったUI更新もよく使われます。新しいWPFアプリでは、Taskasync-awaitを使った実装も検討するとよいでしょう。

12-3. 複数のBackgroundWorkerを同時に実行できますか?

複数のBackgroundWorkerインスタンスを作成すれば、同時に実行することは可能です。

C#
BackgroundWorker worker1 = new BackgroundWorker();
BackgroundWorker worker2 = new BackgroundWorker();

ただし、同時実行数が増えるとCPUやメモリ、ファイルアクセス、DB接続などに負荷がかかります。複数処理を制御する必要がある場合は、TaskSemaphoreSlimなども検討しましょう。

12-4. Thread.Sleepを使っても問題ありませんか?

サンプルコードでは、重い処理の代わりにThread.Sleepを使うことがあります。

C#
Thread.Sleep(1000);

ただし、実際のアプリでは単に待つためだけにThread.Sleepを多用するのはおすすめしません。この記事のサンプルでは、ファイル処理や計算処理の代わりとして動きをわかりやすくするために使っています。

UIスレッド上でThread.Sleepを使うと画面が固まる原因になりますが、DoWork内で重い処理の模擬として使う分には学習用サンプルとして理解しやすいです。

12-5. async-awaitとBackgroundWorkerはどちらを使うべきですか?

新規開発では、基本的にasync-awaitを優先して検討するとよいです。

一方で、次のような場合はBackgroundWorkerも選択肢になります。

状況選択肢
既存WinFormsアプリの保守BackgroundWorker
すでにBackgroundWorkerが使われている既存方針に合わせる
簡単な進捗表示を追加したいBackgroundWorker
新規開発で非同期I/Oが中心async-await
複雑なキャンセルや並列処理Task / async-await

初心者が「UIスレッドを止めない」「重い処理を別に分ける」という考え方を学ぶには、BackgroundWorkerはよい教材になります。

12-6. ProgressBarが更新されないときは何を確認すべきですか?

ProgressBarが更新されない場合は、次を確認してください。

確認項目内容
WorkerReportsProgresstrueになっているか
ReportProgressDoWork内で呼んでいるか
ProgressChangedイベント登録されているか
progressBar1.Maximum通常は100になっているか
Valueの範囲0〜100の範囲内か
UIスレッドで更新しているかProgressChangedで更新しているか
処理が速すぎないか一瞬で終わって見えないだけではないか

基本的な実装は次の形です。

C#
worker.WorkerReportsProgress = true;
C#
bw.ReportProgress(percent);
C#
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}

この3点がそろっていれば、ProgressBarは更新されるはずです。

まとめ

BackgroundWorkerは、C#のWindows FormsアプリでUIを固めずに重い処理を実行するための便利な仕組みです。特に、初心者が非同期処理、進捗表示、キャンセル、完了通知を理解するうえで扱いやすい構造になっています。

重要なポイントを整理すると、次のとおりです。

ポイント内容
重い処理はDoWorkに書くUIスレッドを止めないため
UI更新はProgressChangedRunWorkerCompletedで行うクロススレッドエラーを防ぐため
開始はRunWorkerAsyncバックグラウンド処理を開始する
進捗はReportProgressProgressBarやLabelに反映する
キャンセルはCancelAsyncCancellationPending強制停止ではなく協調的に止める
結果はe.ResultErrorCancelled確認後に取得する
二重実行はIsBusyで防ぐ実行中の再開始を避ける
新規開発ではasync-awaitも検討現代的な非同期処理に向いている

BackgroundWorkerを正しく使うコツは、「重い処理」と「UI更新」を分けることです。DoWorkでは処理に集中し、画面更新はProgressChangedRunWorkerCompletedに任せることで、UIが固まらない使いやすいアプリを作れます。

既存のWinFormsアプリを改善したい場合や、進捗表示・キャンセル付きの処理を手軽に実装したい場合は、この記事のサンプルコードをベースにBackgroundWorkerを試してみてください。