まえがき
私自身はフロントエンドなにそれ?なWindowsFormアプリケーションと運命を共にする世界線で仕事してますので非同期操作を要求される場面が殆どありません。皆無かと言われるとそうでもないんですけどExcelのデカいシート(数十万セル)の読み込みとかは確かに重たい処理でしょうが結局それを読み込まないと先に進めないのであまり非同期にする意味がない。そんなこんなでTaskを使い始めたのはつい最近のことなので多くの方の記事があるのは承知の上で学習メモも兼ねて。
Task自体がいつから実装されているのかは知らないんですがasync/awaitキーワードが使えるようになったのはVS2012かららしいので何を今さら感全開ですね1。
環境
- OS : Windows10Home(2004)
- IDE : Visual Studio 2019 16.8.4
- .NET Framework : 4.8
非同期にしないとどうなるか
簡単な例でいきましょう。WindowsFormにボタンが一つだけあってボタンをクリックすると5秒後にメッセージボックスを表示します。
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object sender, EventArgs e)
{
Thread.Sleep(5000); // 所謂重たい処理
MessageBox.Show("Done!");
}
}
}
重たい処理の例としてよくSleep()が出てきますが具体的には何かダウンロードしたりDBから検索したり数百ファイルからgrepするとかの数秒から数分行ったっきりになるような処理をイメージしてください。
動かしてみるとわかりますが、この重たい処理をやっている間はフォームの移動もできませんし、他のコントロールが載っていたとしても一切操作できません。ロック状態というやつです。殆どの場合何か重たい処理の結果を見てから次に何かするわけで支障が無いと言えば無いのですが、邪魔だから最小化しておこうとか中断のためのキャンセルボタンを設置しても操作することができません。てなわけで少々具合が悪いです。
Task登場以前はどうしていたか
やり方はいくつかありますが敷居が低いと思われる2BackgroundWorkerを使った例を挙げます。BackgroundWorkerは名前から推測できるように裏(=バックグランド)で動きます。
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp
{
public partial class Form1 : Form
{
private BackgroundWorker bw;
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object sender, EventArgs e)
{
bw = new BackgroundWorker();
bw.DoWork += Bw_DoWork;
bw.RunWorkerCompleted += Bw_RunWorkerCompleted;
bw.RunWorkerAsync();
}
// 重たい処理はこの中で
private void Bw_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(5000);
}
// 処理が終了したらこのイベントが発生する
private void Bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Done!");
}
}
}
上記コードでは「重たい処理を別スレッドで行わせる」「終わったことを知る」しかしていませんが、もう少し書き足せば進捗通知とキャンセル処理を行うことができます。BackgroundWorkerで事足りないこともないのですがいちいちイベントハンドラを書くのが面倒。DoWorkやRunWorkerCompletedの中身が上記のようにちょっとしか無いのであれば
bw.DoWork += delegate { Thread.Sleep(5000); };
bw.RunWorkerCompleted += delegate { MessageBox.Show("Done!"); };
このように匿名メソッドにしてしまってイベントハンドラは書かないのもアリです。
Task(async/await)を使って書いてみる
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private async void Button1_Click(object sender, EventArgs e)
{
var task = Task.Run(() =>
{
Thread.Sleep(5000);
});
await task;
MessageBox.Show("Done!");
}
}
}
どうでしょう。一番最初の同期処理のコードにちょっと書き足すだけで非同期処理になりました。BakgroundWorkerを使った時と同様に5秒待ちしている間もフォームの操作が可能です。Taskに重たい処理をお任せした後はメインスレッドがそのまま走り続けますのでロック状態になりません。
といったところで今回はほんのさわりだけ。