概要
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
メソッドで「継続」呼び出しするのではなく、 即座に続きの処理を行います。
- タスクが完了していれば true を返します。 この場合、後述の
-
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 スレッドで実行しないといけない処理 と書く。