WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?(async/await編)[C#/VB].NET TIPS

タスク並列ライブラリとasync/await機構を使って、バックグラウンド処理を簡潔に記述する方法を解説する。

» 2015年12月02日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4.5以降


 時間のかかる処理(以下、重い処理)はアプリケーションのメインスレッド(UIスレッド)とは別のスレッド(以下、バックグラウンド)で行わなければならない。重い処理によってアプリケーションのユーザーインターフェース(UI)がフリーズすることがないようにするためである。その実現のために、.NET Framework 2.0からはBackgroundWorkerクラス(System.ComponentModel名前空間)が利用されてきた(参照:「.NET TIPS:時間のかかる処理をバックグラウンドで実行するには?[2.0のみ、C#、VB]」)。

 しかし、BackgroundWorkerクラスによる実装は、面倒で処理の流れが把握しにくいものだった。バックグラウンド処理が終わった後に実行される処理は別の場所に書かれるので、コードの流れが分断されていたのである。

 それが、タスク並列ライブラリ(TPL)とVisual Studio 2012で導入されたasync/awaitキーワードを使うことで、簡潔に、しかも処理の流れを追いやすくコーディングできるようになったのだ。本稿では、その方法を解説する。なお、本稿のサンプルは「Windows desktop code samples:.NET Tips #1121」からダウンロードできる。

async/awaitを活用して重い処理をバックグラウンドで実行するには?

 端的にいうと「重い処理をTask.Runメソッドで包んでawait」するだけでよい。

 Visual Studio 2012を使って、Windowsフォームで試してみよう(WPFでも同様である)。まず、次の画像のようなUIを作成する。

作成したWindowsフォーム(Windows 10での実行) 作成したWindowsフォーム(Windows 10での実行)
フォーム上に、ボタンコントロールを二つ(button1とbutton2)、プログレスバーコントロール(toolStripProgressBar1)、ラベルコントロール(toolStripStatusLabel1)を配置した。
実際のコードは、別途公開のサンプルをご覧いただきたい。

 このサンプルプログラムでは、button1ボタンがクリックされると、重い処理を開始する。重い処理は10秒ほどかかるが、その間もフォームを移動したりリサイズしたりできる。処理完了時には、プログレスバーとラベルの表示を切り替え、メッセージボックスを表示する。

 そのコードは次のようになる(button1ボタンのクリックイベントハンドラーと関連するコードだけを示す)。

private async void button1_Click(object sender, EventArgs e)
{
  DisableAllButtons();
  toolStripStatusLabel1.Text = "処理中…";
  toolStripProgressBar1.Value = 0;

  // 時間のかかる処理をUIスレッドで実行
  // string result = DoWork(100);
  // これではフォームがフリーズしてしまう

  // 時間のかかる処理を別スレッドで開始
  string result = await Task.Run(() => DoWork(100));

  // ↑
  // この間10秒ほどかかるが、フォームの移動/リサイズなどは可能
  // ↓

  // 処理結果の表示
  toolStripStatusLabel1.Text = result;
  toolStripProgressBar1.Value = 100;
  MessageBox.Show("正常に完了");

  EableAllButtons();
}

// 時間のかかる処理を行うメソッド
private string DoWork(int n)
{
  // 時間のかかる処理
  for (int i = 1; i <= n; i++)
  {
    System.Threading.Thread.Sleep(100);
  }

  // このメソッドからの戻り値
  return "全て完了";
}

private void DisableAllButtons()
{
  button1.Enabled = false;
  button2.Enabled = false;
}
private void EableAllButtons()
{
  button1.Enabled = true;
  button2.Enabled = true;
}

Private Async Sub Button1_Click(sender As Object, e As EventArgs) _
    Handles Button1.Click
  DisableAllButtons()
  ToolStripStatusLabel1.Text = "処理中…"
  ToolStripProgressBar1.Value = 0

  ' 時間のかかる処理をUIスレッドで実行
  ' Dim result As String = DoWork(100)
  ' これではフォームがフリーズしてしまう

  ' 時間のかかる処理を別スレッドで開始
  Dim result As String = Await Task.Run(Function() DoWork(100))

  ' ↑
  ' この間10秒ほどかかるが、フォームの移動/リサイズなどは可能
  ' ↓

  ' 処理結果の表示
  ToolStripStatusLabel1.Text = result
  ToolStripProgressBar1.Value = 100
  MessageBox.Show("正常に完了")

  EableAllButtons()
End Sub

' 時間のかかる処理を行うメソッド
Private Function DoWork(n As Integer) As String
  ' 時間のかかる処理
  For i As Integer = 0 To n - 1
    System.Threading.Thread.Sleep(100)
  Next

  ' このメソッドからの戻り値
  Return "全て完了"
End Function

Private Sub DisableAllButtons()
  Button1.Enabled = False
  Button2.Enabled = False
End Sub
Private Sub EableAllButtons()
  Button1.Enabled = True
  Button2.Enabled = True
End Sub

async/awaitを活用して重い処理をバックグラウンドで実行する(上:C#、下:VB)
ボタンがクリックされたときのイベントハンドラー「button1_Click」の中で、重い処理「DoWork」メソッドを呼び出したい。しかし、普通に呼び出すとフォームがフリーズしてしまう。そこでこのコードのように、DoWorkメソッドをTask.Runメソッドで包み、awaitキーワードを付けることで、DoWorkメソッドがバックグラウンドで実行されるようになる。なお、awaitキーワードを使うときには、メソッドのシグネチャにasyncキーワードも必要だ。

 BackgroundWorkerクラスを使う方法に比べると、実にシンプルだ。また、イベントハンドラーの中で、重い処理の呼び出し→処理結果の表示という順で(分断されることなく)コードが記述できている。なんといってもうれしいのは、重い処理を行うDoWorkメソッドに何も手を加えなくてよいことだろう。

バックグラウンドの処理から進捗を表示するには?

 以上のように、重い処理のバックグラウンド実行は、async/awaitを使ってとても簡単に記述できる。ただし忘れてならないのは、これによってDoWorkメソッドが別のスレッドで実行されることだ。

 DoWorkメソッドが別のスレッドで実行されていると、その中からUI要素を直接操作できない(例外が発生する)。しかし、10秒もかかるような処理では進捗(しんちょく)を表示すべきだろう。どうしたらよいだろうか?

 このような場合に進捗を表示するには、Progressクラス(System名前空間)を使うとよい(次のコード)。Progressオブジェクトには、進捗をUIに表示するためのメソッドを登録しておく。DoWorkメソッドには、IProgressインターフェース(System名前空間)の引数を追加する。そして、バックグラウンドで実行しているDoWorkメソッドの中でProgressオブジェクトのReportメソッドを呼び出すと、Progressオブジェクトに登録されているメソッドがUIスレッドで実行されるのである。

private async void button2_Click(object sender, EventArgs e)
{
  DisableAllButtons();
  toolStripStatusLabel1.Text = "処理中…";
  toolStripProgressBar1.Value = 0;

  // Progressクラスのインスタンスを生成
  var p = new Progress<int>(ShowProgress);

  // 時間のかかる処理を別スレッドで開始
  string result = await Task.Run(() => DoWork(p, 100));

  // 処理結果の表示
  toolStripStatusLabel1.Text = result;
  toolStripProgressBar1.Value = 100;
  MessageBox.Show("正常に完了");

  EableAllButtons();
}

// 進捗を表示するメソッド(これはUIスレッドで呼び出される)
private void ShowProgress(int percent)
{
  toolStripStatusLabel1.Text = percent + "%完了";
  toolStripProgressBar1.Value = percent;
}

// 時間のかかる処理を行うメソッド(進捗付き)
private string DoWork(IProgress<int> progress, int n)
{
  // 別スレッドで実行されるため、このメソッドでは
  // UI(コントロール)を操作してはいけない

  // 時間のかかる処理
  for (int i = 1; i <= n; i++)
  {
    System.Threading.Thread.Sleep(100);

    int percentage = i * 100 / n; // 進捗率
    progress.Report(percentage);
  }

  // このメソッドからの戻り値
  return "全て完了";
}

Private Async Sub Button2_Click(sender As Object, e As EventArgs) _
    Handles Button2.Click
  DisableAllButtons()
  ToolStripStatusLabel1.Text = "処理中…"
  ToolStripProgressBar1.Value = 0

  ' Progressクラスのインスタンスを生成
  Dim p = New Progress(Of Integer)(AddressOf ShowProgress)

  ' 時間のかかる処理を別スレッドで開始
  Dim result As String = Await Task.Run(Function() DoWork(p, 100))

  ' 処理結果の表示
  ToolStripStatusLabel1.Text = result
  ToolStripProgressBar1.Value = 100
  MessageBox.Show("正常に完了")

  EableAllButtons()
End Sub

' 進捗を表示するメソッド(これはUIスレッドで呼び出される)
Private Sub ShowProgress(percent As Integer)
  ToolStripStatusLabel1.Text = percent & "%完了"
  ToolStripProgressBar1.Value = percent
End Sub

' 時間のかかる処理を行うメソッド(進捗付き)
Private Function DoWork(progress As IProgress(Of Integer), n As Integer) As String
  ' 別スレッドで実行されるため、このメソッドでは
  ' UI(コントロール)を操作してはいけない

  ' 時間のかかる処理
  For i As Integer = 0 To n - 1
    System.Threading.Thread.Sleep(100)

    Dim percentage As Integer = i * 100 \ n ' 進捗率
    progress.Report(percentage)
  Next

  ' このメソッドからの戻り値
  Return "全て完了"
End Function

Progressクラスを使ってバックグラウンドから進捗を表示する(上:C#、下:VB)
前のコードに、太字の部分を追加した。
Progressクラスのインスタンスを生成するときに、進捗を表示する「ShowProgress」メソッドをコンストラクター引数として渡す。バックグラウンドで「DoWork」メソッドを開始するときに、そのProgressオブジェクトを渡す。「DoWork」メソッドの中でProgressオブジェクトのReportメソッドを呼び出すと、UIスレッドで「ShowProgress」メソッドが実行されるという仕組みだ。

 なお、バックグラウンド処理のキャンセルも、進捗表示と似た方法で行う。CancellationToken構造体(System.Threading名前空間)を使うのだが、進捗表示よりは少々複雑になる。詳細は、MSDNドキュメント「How to: Cancel a Task and Its Children」をご覧いただきたい。

まとめ

 重い処理をバックグラウンドで実行するには、「Task.Runで包んでawait」すればよい。また、バックグラウンドでの処理中に進捗表示をするには、IProgress<T>インターフェースを利用する。

利用可能バージョン:.NET Framework 4.5以降
カテゴリ:Windowsフォーム 処理対象:スレッド
カテゴリ:WPF 処理対象:スレッド
使用ライブラリ:Taskクラス(System.Threading.Tasks名前空間)
使用ライブラリ:Progressクラス(System名前空間)
関連TIPS:時間のかかる処理をバックグラウンドで実行するには?[2.0のみ、C#、VB]
関連TIPS:時間がかかる処理での「応答なし」を回避するには?
関連TIPS:WPF:DataGridやListViewなどに表示しているデータを別スレッドから変更するには?[C#、VB]
関連TIPS:スレッド・セーフなコレクション・オブジェクトを作成するには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。