目次

(追記) (追記ここまで)

概要

Ver. 5.0

C# はこれまでも一貫して、「言語自体(コンパイラー)に多くのことをさせ過ぎない」、 「可能な限りフレームワーク側(クラス ライブラリ側)に実装を任せる」という方針で機能追加を行っています。 例えば、foreach や LINQ の実装がその例ですが、以下のように、コンパイラーの仕事はメソッド呼び出しへの変換になります。

  • foreach」は、enumrable/enumerator パターンに沿って実装されたクラスなら何でも列挙可能。

    • 単純に、GetEnumerator メソッドや MoveNext, Current などの呼び出しに置き換えられる。
  • LINQ「クエリ式」は、Select や Where という名前のメソッドを持っていれば何でも問い合わせ可能。

非同期メソッドも同様の方針を取っていて、 本項で説明するようなパターンに沿ったクラスなら、なんでも await の対象にできます。

サンプル

Awaitable パターン

await の対象にできるのは、 以下のような Awaitable パターンを実装したクラスです。 (インターフェイスなどの実装も不要で、いわゆる「ダックタイピング」的。)

// 同名のメソッドを持っていれば型は問わない。
class Awatable
{
 public Awaiter GetAwaiter() { }
}
// 同上、同名のメソッドを持っていれば型は問わない。
struct Awaiter
{
 public bool IsCompleted { get; }
 public void OnCompleted(Action continuation) { }
 public T GetResult() { }
}

await 可能な型は、上記の Awaitable クラスのように、Awaiter を返す GetAwaiter メソッド(あるいは拡張メソッドでも OK)を持つ必要があります。 Awaiter は、以下のようなプロパティ/メソッドを持つ必要があります。

  • bool IsCompleted プロパティ

    • タスクが完了していれば true を返します。 この場合、後述のOnCompletedメソッドで「継続」呼び出しするのではなく、 即座に続きの処理を行います。
  • void OnCompleted メソッド

    • タスクが未完(IsCompletedが false)な場合、 引数で与えた continuation を「継続」登録(例えば Task<T>.ContinueWith に渡す)します。
  • T GetResult()

    • タスクの結果を取り出します。

    • 非同期処理の結果が戻り値を持つ場合 (例えば、 タスクがいわゆる「先物」(ジェネリック版の Task<T> など)の場合)、 結果の値を返します。

    • 非同期処理の結果が戻り値なし(void)の場合、 GetResult メソッドの戻り値も void で、 単にタスクの完了を待ちます。

    • タスク内で例外が発生していた場合、GetResult でその例外を受け取れます(スレッド間の例外の伝搬)。

Task クラスなどに直接 IsCompleted/OnCompleted/GetRusult を持たせるのではなく、 GetAwaiter を挟むことで拡張性を持たせています。 GetAwaiter は拡張メソッドでもいいので、独自実装で挙動を変えるということもしやすくなっています。

サンプル

(参考: サンプルの AwaiterPatternSample プロジェクト。)

実装例を挙げてみましょう。 せっかくの非同期呼び出しを同期化(処理が終わるまでブロッキング)するという、使い道のない実装ですが、 シンプルなのでサンプルとしては分かりやすいと思います。

public class BlockingAwaitable<T>
{
 private BlockingAwaiter<T> _awaiter;
 public BlockingAwaitable(Task<T> task) { _awaiter = new BlockingAwaiter<T>(task); }
 public BlockingAwaiter<T> GetAwaiter() { return _awaiter; }
}
public class BlockingAwaiter<T>
{
 private Task<T> _task;
 public BlockingAwaiter(Task<T> task) { _task = task; }
 public bool IsCompleted { get { return true; } }
 public void OnCompleted(Action continuation) { }
 public T GetResult()
 {
 _task.Wait();
 return _task.Result;
 }
}
public static class BlockingAwaitableExtensions
{
 public static BlockingAwaitable<T> ToBlocking<T>(this Task<T> task)
 {
 return new BlockingAwaitable<T>(task);
 }
}

以下のように利用します。

varresult = await task.ToBlocking();

状態機械生成

それでは、この awaitable/awaiter が実際にどのように利用されているのかを見てみましょう。 仕組みとしては、「イテレーター」と似ていて、 一種の状態機械(state machine)の生成となっています。

イテレーターの場合には、yield return の部分が以下のようなコードに置き換えられます。

state = State1; // 次に復帰するときのための状態の記録
Current = x; // 戻り値を Current に保持
return true; // いったん処理終了
case State1: // 次に呼ばれたときに続きから処理するためのラベル

処理はいったん中断し、次に呼ばれたときには state の値に応じた switch や goto によって、 続きの処理を再開します。

非同期メソッドの場合には、await の部分が以下のようなコードに置き換えられます。

state = State1; // 次に復帰するときのための状態の記録
var task = RunAsync();
var awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
 awaiter.OnCompleted(a); // タスクが未完の場合だけ、継続登録して一度 return
 return;
}
case State1: // 次に呼ばれたときに続きから処理するためのラベル
var y = awaiter.GetReslt(); // タスクの結果を受け取り
awaiter = default(T); // ガベージ コレクションが働きやすくなるように null 代入

このコードはラムダ式で囲われていて、 (BeginAwait の引数となっている)Action 型の変数 a に代入されているものと思ってください。 結果として、タスクの継続として自分自身が呼ばれ、state に応じた switch や goto によって続きの処理が行われます。

ちなみに、awaitable/awaiter を介さない単純な実装に展開するなら、以下のようになります。 (実際には、await は Task クラス以外にも使えますし、単純に ContinueWith を呼ぶより少しだけ複雑な処理(後述の SynchronizationContext を利用)を行っています。)

state = State1; // 次に復帰するときのための状態の記録
var task = AnotherTaskAsync();
if (!task.IsCompleted)
{
 // 他のタスクの完了待ちに入って、いったん処理中止
 task.ContinueWith(a);
 return;
}
// ただし、タスクがすでに完了済みだったら処理続行
case State1: // 次に呼ばれたときに続きから処理するためのラベル
var y = task.Result; // タスクの結果を受け取り
サンプル

(参考: サンプルの PseudoAsync プロジェクト。)

例えば、以下のような非同期メソッドを考えてみましょう。 要は、複数の URL から文字列をダウンロードしてきて表示するプログラムです(ShowTitle の実装については割愛)。

private static async void RunTaskAsync(params string[] uriList)
{
 var client = new WebClient();
 foreach (var uri in uriList)
 {
 var html = await client.DownloadStringTaskAsync(uri);
 ShowTitle(html);
 }
}

非同期メソッドがイテレーターと似たようなコード生成をしているということは、 イテレーターを使って似たようなことができなくもないです。 上記の例は、イテレーターを使って書くと以下のようになります。

private static void RunPseudoAsync(params string[] uriList)
{
 AsyncHelper(RunIterator(uriList));
}
private static IEnumerable<Task> RunIterator(params string[] uriList)
{
 var client = new WebClient();
 foreach (var uri in uriList)
 {
 //↓ここから
 var task = client.DownloadStringTaskAsync(uri);
 if (!task.IsCompleted)
 {
 yield return task;
 }
 var html = task.Result;
 //↑ここまでが await 相当の処理
 ShowTitle(html);
 }
 yield return null;
}
private static void AsyncHelper(IEnumerable<Task> asyncTask)
{
 var e = asyncTask.GetEnumerator();
 Action a = null;
 a = () =>
 {
 if (e.MoveNext() && e.Current != null)
 {
 e.Current.ContinueWith(t => a());
 }
 };
 a();
}

さらに、イテレーター相当の処理も展開すると以下のようになります。

private static void RunAsyncInside(IEnumerable<string> uriList)
{
 Action a = null;
 var e = uriList.GetEnumerator();
 int state = 0;
 WebClient client = null;
 Task<string> task = null;
 a = () =>
 {
 switch(state)
 {
 case 0: goto State0;
 case 1: goto State1;
 }
 State0:
 client = new WebClient();
 // goto の都合上、ループは if goto とか if return に置き換わる。
 if (!e.MoveNext()) return;
 //↓ここから
 state = 1;
 task = client.DownloadStringTaskAsync(e.Current);
 if (!task.IsCompleted)
 {
 task.ContinueWith(t => a);
 return;
 }
 State1:
 var html = task.Result;
 //↑ここまでが await 相当の処理
 ShowTitle(html);
 };
 a();
}

catch句、finally句内でのawait

Ver. 6

C# 6からは、catch句、finally句内にもawaitを書けるようになりました。

これの展開は結構面倒で、ここまでで説明してきたような単純な置き替えルールではできません。追加で、以下のようなことをしています。

  • すべての例外を無差別にcatch
  • catch句内、finally句内相当の処理を実行
  • 例外を再throw

最後の例外の再throwが曲者で、例外のスタック トレースを保ったまま例外をthrowし直すのは結構難しかったりします(.NET Frameworkの内部的な機能(internalなメソッド)を使わないとできなかったりします)。

同期コンテキスト

(書きかけ)

(参考: サンプルの SynchronizationContextSample プロジェクト。)

GUI アプリの場合、UI を更新できるのは UI スレッドだけ。 非同期処理の結果を UI スレッドに返す必要あり。 参考: 「[雑記] GUI と非同期処理

・ディスパッチャーを呼ぶ仕組み
WPF とか Silverlight の場合、継続がディスパッチャー経由で呼ばれる。
SynchronizationContext.Post 経由。
(標準提供の TaskAwaiter がこういう挙動してる。
 気に入らなければ Awaiter の自作で回避可能。)
詰まるところ、いくら await しても UI スレッドに処理戻ってくる。
当然、そこで重たい処理したら UI フリーズするので注意。
(一番向いてる処理は、IO 待ち)
・もし、重たい処理が必要なら
await Task.Run(() =>
{
 // 重たい処理
 // ここは別スレッドで動いてる
}
// SynchronizationContext 経由で UI スレッドに戻る
// UI スレッドで実行しないといけない処理
と書く。

更新履歴

ブログ


誤字等を見つけた場合や、ご意見・ご要望がございましたら、GitHub の Issues まで気兼ねなくご連絡ください。
(追記) (追記ここまで)
(追記) (追記ここまで)
(追記) (追記ここまで)
(追記) (追記ここまで)
(追記) (追記ここまで)
(追記) (追記ここまで)

AltStyle によって変換されたページ (->オリジナル) /