最近の記事「Windows ランタイムの非同期性により高速で滑らかなアプリにする」では、C# と Visual Basic の await キーワードを使用して、そのままの制御フローを維持しながら、WinRT の非同期操作を使用できることを示すサンプルをご紹介しました。

この続きの記事では、WinRT で実際に await がどのように機能するかについて、内部を詳しく見ていきます。この知識を身に付けておくと、await を使うコードの動作を理解しやすくなり、結果として、より優れた Metro スタイル アプリを作成できるようにもなります。

手始めに、await を使わない場合について基本を確認しましょう。

基本のおさらい

WinRT の非同期性は、すべて IAsyncInfo という 1 つのインターフェイスに基づいています。

public interface IAsyncInfo
{
AsyncStatus Status { get; }
HResult ErrorCode { get; }
uint Id { get; }

void Cancel();
void Close();
}

WinRT におけるすべての非同期操作は、このインターフェイスを実装しています。このインターフェイスは、非同期操作を参照してその ID と状態を問い合わせたり、操作のキャンセルを要求したりするために必要な基本機能を提供します。ただしこのインターフェイスには、非同期処理において最も重要とも言える部分が欠けており、操作の完了をリスナーに通知するコールバックが用意されていません。この機能は、IAsyncInfo を必要とする他の 4 つのインターフェイスに意図的に分離されています。WinRT の非同期操作はすべて、これらの 4 つのインターフェイスのいずれかを実装しています。

public interface IAsyncAction : IAsyncInfo
{
AsyncActionCompletedHandler Completed { get; set; }
void GetResults();
}

public interface IAsyncOperation<TResult> : IAsyncInfo
{
AsyncOperationCompletedHandler<TResult> Completed { get; set; }
TResult GetResults();
}

public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo
{
AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; }
AsyncActionProgressHandler<TProgress> Progress { get; set; }
void GetResults();
}

public interface IAsyncOperationWithProgress<TResult, TProgress> : IAsyncInfo
{
AsyncOperationWithProgressCompletedHandler<TResult, TProgress> Completed { get; set; }
AsyncOperationProgressHandler<TResult, TProgress> Progress { get; set; }
TResult GetResults();
}

これらの 4 つのインターフェイスでは、結果がある場合とない場合、進行状況のレポートがある場合とない場合のすべての組み合わせがサポートされます。どのインターフェイスも Completed プロパティを公開しており、操作の完了時に呼び出されるデリゲートを設定することができます。デリゲートを設定できるのは 1 回だけです。操作の完了後にデリゲートを設定した場合は、操作の完了とデリゲートの割り当てのタイミングの差に対処する実装によって、すぐにデリゲートがスケジュールされるか呼び出されます。

たとえば、XAML ボタンのある Metro スタイル アプリを実装しようとしているとしましょう。そのボタンがクリックされたら、WinRT スレッド プールのキューに処理を追加して、計算に時間のかかる操作を実行します。処理が完了したら、ボタンのコンテンツを更新して操作の結果を表示します。

これを実装するには、どのような方法があるでしょうか。WinRT の ThreadPool クラスには、プール上の処理を非同期的に実行するメソッドが用意されています。

public static IAsyncAction RunAsync(WorkItemHandler handler);

このメソッドを使用すると、計算に時間のかかる処理をキューに追加して、その実行中に UI スレッドがブロックされるのを防ぐことができます。

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
}

これで UI スレッドからプールにうまく処理を移すことができましたが、処理の完了を知るにはどうすればよいでしょうか。RunAsyncIAsyncAction を返すので、完了ハンドラーを使って通知を受け取り、その通知に応答して続きの処理を実行することができます。

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
btnDoWork.Content = result.ToString(); // bug!
};
}

この場合、ThreadPool に追加された非同期操作が完了すると、Completed ハンドラーが呼び出されます。ハンドラーでは結果をボタンに格納しようとしていますが、残念ながらこれは正しく動作しません。どうやら Completed ハンドラーは、UI スレッドから呼び出されるわけではなさそうです。btnDoWork.Content を変更するためには、ハンドラーが UI スレッドで実行されていなければなりません (そうでない場合、エラー コード RPC_E_WRONG_THREAD の例外が発生します)。これに対処するには、UI に関連付けられている CoreDispatcher オブジェクトを使用して、呼び出しを元のスレッドに適切にマーシャリングします。

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
btnDoWork.Content = result.ToString();
});
};
}

これで動作するようになりました。しかし、Compute メソッドから例外がスローされた場合はどうなるでしょうか。または、ThreadPool.RunAsync から返された IAsyncAction を通じて Cancel が呼び出された場合はどうでしょう。Completed ハンドラーでは、IAsyncAction の終了状態には CompletedErrorCanceled の 3 つがあり、そのいずれかで終了するという点に対処しなければなりません。

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); })
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
switch (asyncStatus)
{
case AsyncStatus.Completed:
btnDoWork.Content = result.ToString();
break;
case AsyncStatus.Error:
btnDoWork.Content = asyncAction.ErrorCode.Message;
break;
case AsyncStatus.Canceled:
btnDoWork.Content = "A task was canceled";
break;
}
});
};
}

1 回の非同期呼び出しを処理するために、これだけの分量のコードを記述するのはなかなかたいへんです。非同期操作を何度も連続で実行する必要があるとしたらどうなるか想像してみてください。代わりに、次のようなコードで済ますことができたらよいと思いませんか。

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}

このコードは前のコードとまったく同じように動作しますが、完了コールバックを手動で処理する必要はありません。手動で UI スレッドにマーシャリングする必要もありません。完了ステータスを明示的にチェックする必要もありません。さらに、制御フローを大きく変える必要もないので、他の操作を追加して拡張するのも簡単です。たとえば次のように、ループ処理で複数回計算を実行しながら UI を更新することもできます。

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
for(int i=0; i<5; i++)
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();

}
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}

手動で IAsyncAction を使って同じことを達成するとしたらどのようなコードを書く必要があるか、少しだけ考えてみてください。これが、C# と Visual Basic に導入された新しい async/await キーワードの力です。まるで魔法のようでしょう。このコードはそのまま実際に使用できますが、実際のところは魔法でも何でもありません。この記事の残りの部分では、背後でどのような処理が行われているかを見ていきます。

コンパイラによる変換

メソッドを async キーワードでマークすると、C# コンパイラや Visual Basic コンパイラは、ステート マシンを使ってメソッドの実装を書き直します。このステート マシンを使用して、コンパイラは、スレッドをブロックすることなくメソッドの実行を一時停止したり再開したりできるポイントをメソッドに埋め込みます。これらのポイントは適当に挿入されるわけではなく、明示的に await キーワードが使われた場所にのみ挿入されます。

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await something; // <-- potential method suspension point
...
}

まだ完了していない非同期操作を待機するとき、コンパイラによって生成されたコードでは、メソッドに関連付けられたすべての状態 (ローカル変数など) が確実にパッケージされ、ヒープ上に保持されます。その後、関数は呼び出し元に戻り、元のスレッドで別の処理を実行できるようになります。待機中の非同期操作が後で完了すると、保持されていた状態を使ってメソッドの実行が再開されます。

await パターンを公開する型はすべて、待機の対象になることができます。このパターンでは、GetAwaiter メソッドを公開し、そのメソッドから IsCompletedOnCompletedGetResult という各メンバーを含む型を返すというのが基本的な構成です。たとえば、次のようなコードを書いたとします。

await something;

するとコンパイラは、something インターフェイスの各メンバーを使うコードを生成します。そのコードでは、オブジェクトが既に完了しているかどうかがチェックされ (IsCompleted を使用)、まだ完了していない場合は、タスクが最終的に完了したときにコールバックされる継続処理が登録されます (OnCompleted を使用)。操作の完了後は、操作から生成された例外がすべて伝達され、結果が返されます (GetResult を使用)。例として、次のコードを見ていきましょう。

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await ThreadPool.RunAsync(delegate { result = Compute(); });
...
}

コンパイラは、このコードを次のようなコードに書き換えます。

private class btnDoWork_ClickStateMachine : IAsyncStateMachine
{
// Member fields for preserving locals and other necessary state
int $state;
TaskAwaiter<string> $awaiter;
int result;
...
// Method that moves to the next state in the state machine
public void MoveNext()
{
// Jump table to get back to the right statement upon resumption
switch (this.$state)
{
...
case 2: goto Label2;
...
}
...
// Expansion of await ...;
var tmp1 = ThreadPool.RunAsync(delegate { this.result = Compute(); });
this.$awaiter = tmp1.GetAwaiter();
if (!this.$awaiter.IsCompleted)
{
this.$state = 2;
this.$awaiter.OnCompleted(MoveNext);
return;
Label2:
}
this.$awaiter.GetResult();
...
}
}

btnDoWork_Click メソッドに対して、コンパイラは、MoveNext メソッドを含むステート マシン クラスを生成します。MoveNext が呼び出されると、そのたびに btnDoWork_Click メソッドの実行が再開され、次の未完了の await に到達するかメソッドの終わりに達するまで継続されます。コンパイラ生成のコードは、待機が必要な未完了のインスタンスが見つかると、現在の位置と状態変数をマークし、待機中のインスタンスが完了したときに継続するメソッドの実行をスケジュールして制御を戻します。待機中のインスタンスが最終的に完了すると、MoveNext メソッドが再度呼び出され、メソッド内の前回実行が中断された位置にジャンプします。

コンパイラにとって、ここで IAsyncAction が待機されているという点は重要ではありません。コンパイラにとって重要なのは、バインド先に適切なパターンが用意されているという 1 点だけです。IAsyncAction インターフェイスがどのようなものかは既にお話ししたので、コンパイラが期待するような GetAwaiter メソッドは含まれていないことはおわかりでしょう。では、どうしてこれが問題なくコンパイルされて動作するのでしょうか。これをよく理解するためには、まず .NET の Task 型および Task<TResult> 型 (Framework における非同期操作のコア表現)、そしてこれらの型と await との関連を理解する必要があります。

タスクへの変換

.NET Framework 4.5 には、Task および Task<TResult> のインスタンスの待機をサポートするために必要な型とメソッドがすべて用意されています (Task<TResult>Task から派生します)。TaskTask<TResult> は、どちらも GetAwaiter インスタンス メソッドを公開します。これらのメソッドは、それぞれ TaskAwaiter 型と TaskAwaiter<TResult> 型を返します。これらの型は、必要とされる IsCompletedOnCompletedGetResult の各メンバーを公開するため、C# と Visual Basic のコンパイラの要件が満たされます。IsCompleted は、このプロパティがアクセスされた時点でタスクの実行が完了しているかどうかを示すブール値を返します。OnCompleted は、タスクの完了時に呼び出される継続デリゲートを設定します (OnCompleted が呼び出されたときに既にタスクが完了している場合は、継続デリゲートが非同期的に実行されるようにスケジュールされます)。GetResult は、タスクが TaskStatus.RanToCompletion 状態で終了した場合は結果を返し (非ジェネリックの Task 型では void になります)、タスクが TaskStatus.Canceled 状態で終了した場合は OperationCanceledException をスローし、タスクが TaskStatus.Faulted 状態で終了した場合は失敗の原因を表す例外をスローします。

カスタム型で待機をサポートするには、大きく 2 つの方法があります。1 つ目は、カスタムの awaitable 型に await パターン全体を手動で実装し、GetAwaiter メソッドを提供する方法です。このメソッドから、継続処理や例外の伝達などにどのように対処するかを定義するカスタムの awaiter 型を返します。2 つ目は、カスタム型からタスクに変換する処理を実装し、タスクを待機するための組み込みサポートに任せる方法です。これにより、独自の特殊な型を待機できます。後者のアプローチについて見ていきましょう。

.NET Framework には、このような変換を簡単にする TaskCompletionSource<TResult> という型が用意されています。TaskCompletionSource<TResult> によって作成される Task<TResult> オブジェクトは、SetResultSetExceptionSetCanceled の各メソッドを提供します。これらのメソッドは、対応するタスクがいつ、どのような状態で完了するかを直接制御するために使われます。TaskCompletionSource<TResult> は、WinRT の非同期操作など、他のなんらかの非同期操作を表すための一種の shim またはプロキシとして使うことができます。

WinRT の操作を直接的に待機できることは既にご存じのとおりですが、ここではそれを知らないふりをしましょう。この場合、どうすればよいでしょうか。

IAsyncOperation<string> op = SomeMethodAsync();
string result = await ...; // need something to await

これを達成するには、TaskCompletionSource<TResult> を作成し、それを WinRT の非同期操作を表すプロキシとして使用して、対応するタスクを待つ方法があります。試しにやってみましょう。まず、Task を待機できるように、TaskCompletionSource<TResult> をインスタンス化する必要があります。

IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
...
string result = await tcs.Task;

次に、先ほど紹介した WinRT 非同期操作の Completed ハンドラーを手動で使う場合のサンプルと同じように、非同期操作にコールバックをフックする必要があります。これで、操作の完了を知ることができるようになります。

IAsyncOperation<string> op = SomeMethodAsync();

var tcs = new TaskCompletionSource<TResult>();

op.Completed = delegate
{
...
};
string result = await tcs.Task;

そのコールバックでは、IAsyncOperation<TResult> の完了状態をタスクに転送する必要があります。

IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
op.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
string result = await tcs.Task;

これで完了です。WinRT の非同期操作では、操作の完了後に Completed ハンドラーを追加した場合でも、そのハンドラーが適切に呼び出されることが保証されます。したがって、操作の完了前にハンドラーが登録されるように特別な処理を行う必要はありません。操作の完了後に Completed ハンドラーへの参照を破棄する手続きも WinRT の非同期操作が面倒を見てくれるので、ハンドラーが呼び出されたときに Completed を null に設定する必要はありません。実際、Completed は 1 回しか設定できないため、いったん設定した後にもう一度設定しようとするとエラーが発生します。

このアプローチでは、WinRT の非同期操作の完了状態とその操作を表すタスクの完了状態は 1 対 1 で対応付けられます。

最終的な AsyncStatus

対応する TaskStatus

待機の結果

Completed

RanToCompletion

操作の結果を返す (または void)

Error

Faulted

失敗した操作の例外をスローする

Canceled

Canceled

OperationCanceledException をスローする

ここで記述したのは 1 つの await を処理する定型的なコードですが、WinRT の非同期操作を待機するたびにこれを書かなければならないとしたら、すぐにうんざりしてしまうのは目に見えています。優秀なプログラマとしては、この定型コードを、繰り返し呼び出せるメソッドにカプセル化したいと思います。そこで、WinRT の非同期操作をタスクに変換する拡張メソッドを作成しましょう。

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation)
{
var tcs = new TaskCompletionSource<TResult>();
operation.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
return tcs.Task;
}

この拡張メソッドを使えば、次のようなコードを記述できます。

IAsyncOperation<string> op = SomeMethodAsync();
string result = await op.AsTask();

または、もっとシンプルな書き方もできます。

string result = await SomeMethodAsync().AsTask();

ずっと良くなりました。このキャストに似た AsTask 機能は、C# や Visual Basic から WinRT を使うすべての開発者にとってきわめて需要の高いものと考えられるため、実際には皆さんが独自の実装を書く必要はありません。.NET 4.5 には、このようなメソッドが既に組み込まれています。System.Runtime.WindowsRuntime.dll アセンブリには、WinRT の非同期インターフェイスのための次の拡張メソッドが含まれています。

namespace System
{
public static class WindowsRuntimeSystemExtensions
{
// IAsyncAction

public static Task AsTask(
this IAsyncAction source);
public static Task AsTask(
this IAsyncAction source,
CancellationToken cancellationToken);

// IAsyncActionWithProgress

public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
IProgress<TProgress> progress);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);

// IAsyncOperation

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source);
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source,
CancellationToken cancellationToken);

// IAsyncOperationWithProgress

public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
IProgress<TProgress> progress);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);

...
}
}

4 つのインターフェイスのそれぞれには、先ほどゼロから記述したメソッドと同じような、パラメーターのない AsTask オーバーロードが用意されています。その他に、CancellationToken を受け取るオーバーロードもあります。このトークンは、コンポーザブルで強調的なキャンセルを実現するために .NET でよく使われるメカニズムです。すべての非同期操作にトークンを渡すことで、キャンセルが要求されたときに、すべての非同期操作にキャンセルの要求が通知されるようにします。説明のために、AsTask(CancellationToken) オーバーロードを独自に記述するとしたらどうなるかを考えてみましょう (そのような API は既に用意されているわけですが)。CancellationToken には、キャンセルの要求時に呼び出されるデリゲートを受け取る Register メソッドがあります。デリゲートを設定して IAsyncInfo オブジェクトの Cancel を呼び出すだけで、キャンセル要求が転送されます。

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation,
CancellationToken cancellationToken
{
using(cancellationToken.Register(() => operation.Cancel()))
return await operation.AsTask();
}

.NET 4.5 に含まれている実装はこれとまったく同じではありませんが、論理的には同等です。

IAsyncActionWithProgress<TProgress>IAsyncOperationWithProgress<TResult,TProgress> には、IProgress<TProgress> を受け取るオーバーロードもあります。これらのメソッドが受け取る IProgress<T> は、進行状況をレポートするための .NET インターフェイスです。AsTask メソッドは単に、WinRT 非同期操作の Progress プロパティにデリゲートを結び付けて、進行状況の情報が IProgress に転送されるようにします。ここでも、手動で実装した場合の例を見てみましょう。

public static Task<TResult> AsTask<TResult,TProgress>(
this IAsyncOperationWithProgress<TResult> operation,
IProgress<TProgress> progress
{
operation.Progress += (_,p) => progress.Report(p);
return operation.AsTask();
}

WinRT の非同期操作の直接待機

前のセクションでは、WinRT の非同期操作を表すタスクを作成して、それらのタスクを待機する方法を見てきました。しかし、WinRT の操作を直接的に待機することはできないのでしょうか。つまり、次のように記述できたら便利だと思います。

await SomeMethodAsync().AsTask();

さらに、CancellationTokenIProgress<T> を指定する必要がない場合は、AsTask の呼び出しも省略できたら良いと思いませんか。

await SomeMethodAsync();

この記事の最初の方で見たとおり、これはもちろん可能です。コンパイラの要件を思い出してください。コンパイラは、適切な awaiter 型を返す GetAwaiter メソッドを探します。先ほど説明した System.Runtime.WindowsRuntime.dllWindowsRuntimeSystemExtensions の 4 つの WinRT 非同期インターフェイスには、ちょうどそのような GetAwaiter 拡張メソッドが含まれています。

namespace System
{
public static class WindowsRuntimeSystemExtensions
{
...
public static TaskAwaiter GetAwaiter(
this IAsyncAction source);
public static TaskAwaiter<TResult> GetAwaiter<TResult>(
this IAsyncOperation<TResult> source);
public static TaskAwaiter GetAwaiter<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static TaskAwaiter<TResult> GetAwaiter<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
}
}

ここで、各メソッドからの戻り値の型が TaskAwaiterTaskAwaiter<TResult> である点に注目してください。これらのメソッドはそれぞれ、Framework に組み込まれている既存のタスク awaiter を利用します。AsTask に関する今までの説明から、これらのメソッドがどのように実装されているかは想像に難くないと思います。Framework での実際の実装は、次のコードとほとんど変わりません。

public static TaskAwaiter GetAwaiter(
this IAsyncAction source)
{
return source.AsTask().GetAwaiter();
}

つまり、次の 2 行はどちらもまったく同じ動作になります。

await SomeMethodAsync().AsTask();
await SomeMethodAsync();

待機の動作のカスタマイズ

既に説明したとおり、TaskAwaiterTaskAwaiter<TResult> には、コンパイラが awaiter に要求するメンバーがすべて用意されています。

bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); //returns void on TaskAwaiter

ここで最も興味深いメンバーは OnCompleted です。これは、待機中の操作が完了したときに継続デリゲートの呼び出しを行うメンバーです。OnCompleted では、継続デリゲートが適切な場所で実行されるように、特別なマーシャリング動作が行われます。

既定では、タスクの awaiter の OnCompleted が呼び出されると、現在の SynchronizationContext が参照されます。これは、コードが実行されている環境の抽象表現です。Metro スタイル アプリの UI スレッドの場合、SynchronizationContext.Current は、内部の WinRTSynchronizationContext 型のインスタンスを返します。SynchronizationContext には、デリゲートを受け取って適切な場所で実行する Post という仮想メソッドがあります。WinRTSynchronizationContext の場合は、CoreDispatcher をラップし、その RunAsync を使用して、デリゲートを UI スレッド上で非同期的に呼び出します (この記事で既に紹介した手動による方法と同様です)。待機中のタスクが完了すると、OnCompleted に渡されたデリゲートは、OnCompleted が呼び出された時点の環境を捕捉した SynchronizationContextPost に渡されて実行されます。UI ロジックの中で、適切なスレッドへのマーシャリングを気にすることなく await を使ってコードを記述できるのは、このようなしくみのおかげです。つまり、タスクの awaiter が代わりに処理してくれるのです。

もちろん、この既定のマーシャリング動作では望ましくない状況もあり得ます。 そのような状況はライブラリでよく発生します。ライブラリにはさまざまな種類がありますが、多くの場合、UI コントロールの操作や、コードが実行されている特定のスレッドについては考慮されません。これはパフォーマンスの観点から有効であり、スレッド間のマーシャリングに伴うオーバーヘッドを避けることができます。既定のマーシャリング動作を無効にしたい場合のために、TaskTask<TResult> には ConfigureAwait メソッドが用意されています。ConfigureAwait は、ブール値の continueOnCapturedContext パラメーターを受け取ります。true を渡し��場合は既定の動作を使うことを意味し、false を渡した場合は、デリゲートの呼び出しをシステムで強制的に元のコンテキストにマーシャリングする必要はなく、適切と考えられる任意の場所でデリゲートを実行できることを示します。

つまり、WinRT の操作を待機するとき、完了後の処理を元の UI スレッドで実行する必要がない場合は、

await SomeMethodAsync();

または

await SomeMethodAsync().AsTask();

と記述する代わりに、次のように書くことができます。

await SomeMethodAsync().AsTask()
.ConfigureAwait(continueOnCapturedContext:false);

または、単に次のようにも記述できます。

await SomeMethodAsync().AsTask().ConfigureAwait(false);

AsTask を使うべき状況

WinRT の非同期操作を呼び出して完了を待つだけでよい場合は、WinRT の非同期操作を直接的に待機するのが最も簡単でクリーンな方法です。

await SomeMethodAsync();

ただし、それ以上の制御を行う場合は、AsTask を使う必要があります。これが役に立つ状況については、既にいくつか取り上げてきました。

  • CancellationToken を使ってキャンセルをサポートする場合

    CancellationToken token = ...;
    await SomeMethodAsync().AsTask(token);

  • IProgress<T> を使って進行状況のレポートをサポートする場合
    IProgress<TProgress> progress = ...;
    await SomeMethodAsync().AsTask(progress);
  • ConfigureAwait を使って継続処理に対する既定のマーシャリング動作を抑制する場合
    await SomeMethodAsync().AsTask().ConfigureAwait(false);

これ以外にも、AsTask が役に立つ可能性のある重要な状況がいくつかあります。

その 1 つは、Task では複数の継続処理をサポートできるという点に関係しています。WinRT の非同期操作型では、Completed に登録した 1 つのデリゲートしかサポートされません (Completed はイベントというよりもプロパティです)。しかも、これは一度しか設定できません。一般には操作を 1 回だけ待機すればよい場合が多いので、ほとんどの状況ではこれで十分です。たとえば、次の同期メソッドを呼び出す場合を考えましょう。

SomeMethod();

この呼び出しの代わりに、対応する非同期バージョンのメソッドを呼び出して待機するとします。

await SomeMethodAsync();

この場合でも、同期バージョンを使った場合と論理的には同じ制御フローが保たれます。しかしときには、複数のコールバックを登録したり、同じインスタンスを複数回待機したりできることが求められる場合もあります。WinRT の非同期インターフェイスとは対照的に、Task 型は何度でも待機することができます。また、その ContinueWith メソッドも何度でも使うことができ、任意の数のコールバックがサポートされます。要するに、WinRT の非同期操作にコールバックを直接設定するのではなく、AsTask を使って WinRT の非同期操作を表すタスクを取得すれば、その Task に複数のコールバックを設定できます。

var t = SomeMethodAsync().AsTask();
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });

AsTask が役立つもう 1 つの例としては、Task 型または Task<TResult> 型の観点から動作するメソッドを扱う場合が当てはまります。Task.WhenAllTask.WhenAny のような連結子メソッドは、WinRT の非同期インターフェイスではなく、Task に基づいて動作します。つまり、複数の WinRT 非同期操作を呼び出し、いずれかまたはすべてが完了するまで await で待機する場合は、AsTask を使用する方が簡単です。たとえば次の await は、指定された 3 つの操作のいずれかが完了するとすぐに終わり、完了した操作を表す Task を返します。

Task firstCompleted = await Task.WhenAny(
SomeMethod1Async().AsTask(),
SomeMethod2Async().AsTask(),
SomeMethod3Async().AsTask());

まとめ

非同期操作に関連する WinRT の機能の充実には目を見張るものがあります。公開される API の量は、プラットフォームにとって応答性がどれほど重要なものかを物語っています。結果として、非同期操作を扱うプログラミング モデルにも高度な要求が課され、C# および Visual Basic コードのためには awaitAsTask が用意されました。これらの機能が実際にどのように動作するかをつかみ、Metro スタイル アプリを効率よく開発するための参考として、この記事でご紹介した背景的内容がお役に立つことを願っています。

より詳しい情報については、次のリソースをお勧めします。

Stephen Toub
Visual Studio