非同期メソッドを学ぶにあたり、簡単に.NET Frameworkがこれまで提供してきた非同期処理について復習しておきましょう。図示すると大体以下のような感じだと思います。
以下、これらについてコードを交えながら簡単に見ていきます。
UIスレッド上で処理する書き方
もはや言うまでもありませんが、UIスレッドに対して同期的な書き方をすると以下のようになります。これを基本として、以下いろいろな非同期処理の記述方法を確認します。
private void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; Thread.Sleep(3000); //--- 何か長い処理 this.button.IsEnabled = true; }
Thread
スレッドそのもの表すThreadクラスを利用したものです。Threadクラスのインスタンスを生成するとスレッドがひとつ生成されます。下記の例のようにラムダ式とクロージャを利用すれば可能ですが、デリゲートを渡すような場合は戻り値を受けるのが困難になるという欠点があります。また、UIコンポーネントの処理をする場合はDispatcher.BeginInvokeやControl.Invokeなどを利用しなければならず、結構煩わしいです。何より、スレッドそのものに触れている感じが何かイヤ...
private void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; var thread = new Thread(() => { Thread.Sleep(3000); this.Dispatcher.BeginInvoke((Action)(() => { //--- Dispatcherを利用してUIスレッドに処理を配送 this.button.IsEnabled = true; })); }); thread.Start(); }
ThreadPool
スレッドの利用には大変コストがかかります。メモリ消費量の増大 (1スレッドにつき約1MBのメモリを消費) とパフォーマンスの低下 (スレッドの生成/破棄の度にプロセス内でロードされている全dllのDllMain関数が呼び出される) を招きます。スレッドプール (ThreadPoolクラス) はこれらの改善のために考えられたもので、生成したスレッドを効率的に管理してくれます。なので、スレッドを明示的に作成しなければならないケースを除いてはスレッドプールを利用するのがベターです。
private void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(3000); this.Dispatcher.BeginInvoke((Action)(() => { this.button.IsEnabled = true; })); }, null); }
Asynchronous Programming Model
.NET Frameworkで初めて提供された、スレッド自体を直接意識しない非同期処理システムです。下記の例のようにBeginXxx/EndXxxのペアで実装し、引数や戻り値を受けることができます。ただ、お察しの通り記述方法がかなり面倒くさいです。
private void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; var method = new Func<double, double>(param => { Thread.Sleep(3000); return Math.PI * param; //--- 戻り値を返せる }); method.BeginInvoke(Math.E, ar => //--- パラメータを渡せる { var result = method.EndInvoke(ar); //--- 結果 : π × e this.Dispatcher.BeginInvoke((Action)(() => { this.button.IsEnabled = true; })); }, null); }
Event-based Asynchronous Pattern
Windows FormsやWeb Formsが全盛期だった頃に出てきた、非同期処理の完了イベントなどを発生させるイベント主体の記述方法です。メソッド毎に作業を意味的に分離しやすく、比較的見通しが良いのが特徴です。また、完了時のイベントはUIスレッド上で動作するため、わざわざDispatcher.BeginInvokeなどと書く必要がありません。とはいえ、まだちょっと手間な感じはあります。
private readonly BackgroundWorker worker = new BackgroundWorker(); public MainWindow() { this.InitializeComponent(); this.worker.DoWork += this.OnDoWork; this.worker.RunWorkerCompleted += this.OnRunWorkerCompleted; } private void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; this.worker.RunWorkerAsync(Math.E); //--- パラメータを渡せる } private void OnDoWork(object sender, DoWorkEventArgs e) { Thread.Sleep(3000); e.Result = Math.PI * (double)e.Argument; //--- 結果を返せる } private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { var result = e.Result; //--- 結果 : π × e this.button.IsEnabled = true; //--- このメソッドはUIスレッドで動作 }
Task-based Asynchronous Pattern
.NET Framework 4で登場したTask Parallel Libraryの要であるTaskクラスを利用した方法です。Taskクラスとは「何かの処理」を抽象化したものと考えれば良いかと思います。ContinueWithメソッドで「作業が終了したら続けてコレを実行してください」と記述しています。引数のTaskScheduler.FromCurrentSynchronizationContextメソッドにより、「続けて行う処理はUIに同期的に実行してください」と指示しています。これまでの書き方に比べて、非同期処理の意図をかなり分かりやすく表現しているのではないかと思います。
Taskクラスはasync/awaitの要でもありますので、事前準備としてTPL入門もご一読いただけると幸いです。
private void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; Task.Factory.StartNew(() => Thread.Sleep(3000)) .ContinueWith(_ => { this.button.IsEnabled = true; }, TaskScheduler.FromCurrentSynchronizationContext()); }
Reactive Extensions
イベントや非同期処理をLINQの形で記述する方法です。イベントの関連付けも、スレッド間の移動もすべてメソッドチェインとして連続的に書けます。.NET Framework 3.5から利用できるので、async/awaitが利用できない環境下では最善の実装方法と言えるのではないかと思います。
Reactive Extensionsについては、Rx入門も読んで頂けると嬉しいです。
public MainWindow() { this.InitializeComponent(); Observable .FromEventPattern(this.button, "Click") .Do(_ => this.button.IsEnabled = false) .ObserveOn(Scheduler.ThreadPool) .Do(_ => Thread.Sleep(3000)) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => this.button.IsEnabled = true); }
async/await
.NET Framework 4.5 / C# 5.0から提供される予定の非同期メソッドでの記述方法です。もはや最初のUI同期のコードとほとんど変わりません。これだけ簡素化された記述ができるなら、率先して利用したいですね!
private async void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; await Task.Run(() => Thread.Sleep(3000)); this.button.IsEnabled = true; }
これからしばらくは、この非同期メソッドの書き方や内部実装について触れて行こうと思います。