注意: 2010年10月時点での CTP (community technology preview)版を元にした記事になっています。 製品版までに変更の可能性があります。 (async や await というキーワードも変更される可能性あり。)
Ver. 5.0
スレッドを使った非同期処理を行いたい動機としては、以下の2つが挙げられます。
このうち、並列処理に関しては、Parallel クラスや Parallel LINQ で簡単に対応可能 (ラムダ式や LINQ を使えば、並列じゃない場合とほとんど変わらず書けます。 参考: 「[雑記] スレッド プールとタスク」 )。 一方の、非ブロッキング処理は、今までは結構面倒だったものの、 async/await の導入でかなり簡素化されることになります。
URL 指定してダウンロードしてきた文字列をテキストボックスに表示という GUI アプリケーションを考えてみましょう。 同期的に書くなら、ボタンに対して以下のようなイベント ハンドラーを登録します。
private void Button_Click(object sender, RoutedEventArgs e) { var client = new WebClient(); var html = client.DownloadString(this.Url.Text); this.Output.Text = html; }
このように同期でダウンロードを行うと、図1に示すように、ネットワークの通信速度が遅い環境では GUI がフリーズしてしまいます。 そこで、図2に示すように、非同期通信版を使って、UI スレッドをブロッキングしないようにします。
しかし、これまで、非同期呼び出しは少し面倒な書き方をする必要がありました。 いくつかのパターンがありますが、例えば、イベント非同期パターン(EAP: Event-based Asynchronous Pattern)と呼ばれるものの場合、以下のようになります。
private void Button_Click(object sender_, RoutedEventArgs e_) { var client = new WebClient(); client.DownloadStringCompleted += (sender, e) => { this.Output.Text = e.Result; }; client.DownloadStringAsync(new Uri(this.Url.Text)); }
以下のような面倒事が出てきています。
ダウンロード先が1個ならまだましで、例えば、複数の URL からダウンロードしてくる場合にはもっと複雑になります。
private void Button_Click(object sender, RoutedEventArgs e) { var client = new WebClient(); var urlList = this.Url.Text.Split(','); int i = -1; Action<DownloadStringCompletedEventArgs> a = null; client.DownloadStringCompleted += (sender, e) => { var continuation = e.UserState as Action<DownloadStringCompletedEventArgs>; continuation(e); }; a = e => { if (e != null) { this.Output.Text += e.Result; } ++i; if (i >= urlList.Length) { return; } client.DownloadStringAsync(new Uri(urlList[i]), a); }; this.Output.Text = string.Empty; a(null); }
何番目までダウンロード完了したかを自前で状態管理しています。 やり方を知っていれば同期の場合のコードからこのような非同期コードを機械的な手順で書くこともできますが、 手間はかなりかかりますし、可読性は大きく下がります。
C# の新機能で、この手の非ブロッキング処理が簡単になりました。 (おそらく C# 5.0 で。今のところ、バージョンに関する明言なし。)
以下のように、async キーワードや await キーワードを使うことで、 同期っぽい書き方で非同期処理を記述できます。 比較のために、同期版と並べてみましょう。 (背景色を変えて強調表示している部分が同期版との差分です。 この部分を削除すればそのまま同期処理として動きます。)
private void Button_Click(object sender_, RoutedEventArgs e_) { var client = new WebClient(); var html = client.DownloadString(this.Url.Text); this.Output.Text = html; }
private async void Button_Click(object sender_, RoutedEventArgs e_) { var client = new WebClient(); var html = await client.DownloadStringTaskAsync(this.Url.Text); this.Output.Text = html; }
複雑な場合でも、ずいぶんと楽に書けるようになります。 前節の最後で書いた、複数の URL からダウンロードしてくる処理は以下のように書けます。
private async void Button_Click(object sender_, RoutedEventArgs e_) { var client = new WebClient(); var urlList = this.Url.Text.Split(','); this.Output.Text = string.Empty; foreach (var url in urlList) { var html = await client.DownloadStringTaskAsync(url); this.Output.Text += html; } }
同期処理とほとんど同じ書き方ができます。
追加されたのは、async/await の2つのキーワードと、末尾に「TaskAsync」と付いた拡張メソッドです。
foreach (var item in await task) とかも可能。)
前述のとおり、非同期メソッドの戻り値の型は void、Task、もしくは、Task<T> のいずれかである必要があります。
まず、非同期処理を、最終的に Task.Wait メソッドで完了待ちする必要があるかどうかで戻り値の型選びます。 待つ必要があるなら Task もしくは Task<T> に、 必要なければ void にします。
(非同期でない)普通のメソッドから、(戻り値が Task 型の)非同期メソッドの完了を待つには以下のように書きます。
static void Main(string[] args) { RunAsync().Wait(); } static async Task RunAsync() { await TaskEx.Delay(1000); }
ただし、即座に Wait で完了待ちしてしまうと非同期にした意味があまりないので、 通常は、他の作業を並行して行ってから最後に Wait したり、 複数のタスクを同時実行したりします。
var task = RunAsync(); // 並行して別の処理 DoSomeTask(); task.Wait();
// 複数の処理を並行に実行 TaskEx.WhenAll( RunAsync(), RunAsync(), RunAsync()).Wait();
完了待ちが必要ない(戻り値が void)場合というのは、 例えば、GUI アプリケーションのイベント ハンドラーなどで利用します。
private async void Button_Click(object sender, RoutedEventArgs e) { var client = new WebClient(); var html = await client.DownloadStringTaskAsync(this.Url.Text); this.Output.Text = html; }
async(非同期)や await(待つ)という名前に反して、 実は必ずしも非同期実行にはなりません。 というのも、Task クラスの値を await する際、タスクがすでに完了済み(IsCompleted プロパティが true)の可能性もあります。 この場合には、別にタスクを「待つ」必要はないので、 そのまま同期的に処理が続行します。
また、非同期であっても、必ずしもマルチスレッドで実行されるわけではありません。 async/await が Task クラスの上に成り立っているため、 可能な限り同じスレッドを使いまわそうとします。 (参考: 「スレッド プール」 )
(書きかけ)
ITask インターフェイスとかにはしなかった。 Task 的なものの独自実装は結構危険。
↑この資料の9ページ目みたいな問題が。
なので、たぶん、インターフェイスや抽象クラスではなく、具象クラスである Task 固定に。
(書きかけ)
非同期メソッドの引数として、CancellationToke と IProgress を渡す。
CancellationToken を利用。
(参考: サンプルの ProgressSample プロジェクト。)
IProgress インターフェイスと EventProgress クラス BackgroundWorker は、 1. 非同期処理 2. 進捗報告 3. 完了通知 の3つの役目を1つのクラスで負ってて、機能の切り分けがあまりきれいじゃない。 async/await では、 1. 非同期処理を同期っぽく書ける 2. 進捗報告は IProgress インターフェイスを通して行う 3. 完了通知は、同期っぽく、非同期メソッドの最後に書けばそれだけで OK。