Movatterモバイル変換


[0]ホーム

URL:


Logo

目次

概要

Ver. 5.0

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

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

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

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

サンプル

Awaitable パターン

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

// 同名のメソッドを持っていれば型は問わない。classAwatable{publicAwaiter GetAwaiter() { }}// 同上、同名のメソッドを持っていれば型は問わない。structAwaiter{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 classBlockingAwaitable<T>{privateBlockingAwaiter<T> _awaiter;public BlockingAwaitable(Task<T> task) { _awaiter =newBlockingAwaiter<T>(task); }publicBlockingAwaiter<T> GetAwaiter() {return _awaiter; }}public classBlockingAwaiter<T>{privateTask<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 classBlockingAwaitableExtensions{public staticBlockingAwaitable<T> ToBlocking<T>(thisTask<T> task)    {return newBlockingAwaitable<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);// タスクが未完の場合だけ、継続登録して一度 returnreturn;}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 =newWebClient();foreach (var uriin uriList)    {var html =await client.DownloadStringTaskAsync(uri);        ShowTitle(html);    }}

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

private static void RunPseudoAsync(params string[] uriList){    AsyncHelper(RunIterator(uriList));}private staticIEnumerable<Task> RunIterator(params string[] uriList){var client =newWebClient();foreach (var uriin 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 =newWebClient();// 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 スレッドで実行しないといけない処理と書く。

更新履歴

更新:言語バージョンの指定

[C#]

ファイル ベース実行

[C#]

C# 14.0 の新機能

[C#]

更新:[雑記] オーバーロード解決

[C#]

型の分割定義 (partial)

[C#]

ブログ

C# 14 の破壊的変更点(First-class Span)

ファイナライザー

文字列リテラルを data セクションに UTF-8 で書き込む案

nameof(T<>)

First-class な Span 型


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

[8]ページ先頭

©2009-2025 Movatter.jp