Menu


C# 5の非同期、パート2: awaitはどこから?

原文

我々のユーザビリティ研究がこれらは分かりにくいと示したので、私は2つのものについて全く明らかにはっきりすることから始めたい。前回の我々の小さいプログラムを覚えているだろうか?

async void ArchiveDocuments(List urls)
{
  Task archive = null;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = await FetchAsync(urls[i]);
    if (archive != null)
      await archive;
    archive = ArchiveAsync(document);
  }
}

2つのものは以下の通りである:

1) メソッドの「async」修飾子は、「このメソッドは、ワーカー・スレッド上で非同期的に動作するよう自動的に予定される」ことを意味しない。これはその正反対を意味する;これが意味するのは、「このメソッドは非同期操作を待つ必要がある制御フローを含んでいるので、非同期処理が適切な点でこのメソッドを再び始めることができることを確実とするために、コンパイラによって継続渡しスタイルに書き直される。」だ。非同期メソッドの全部の点において、あなたはできる限り現在のスレッド上にいる。それらは、コルーチンのようである:非同期メソッドは、C#にシングルスレッドの協調マルチタスキングを持ってくる。(後日、推論するのではなくasync修飾子を必要とすることの理由を検討する。)

2) そのメソッドで2度使われた「await」演算子は「非同期処理がリターンするまで、今からこのメソッドは現在のスレッドをブロックする」ことを意味しない。それは非同期操作を同期の操作にすることで、それはまさに我々が避けようとしているものである。むしろ、これはその正反対を意味する;これが意味するのは、「我々が待っているタスクがまだ完了しなかったならば、このメソッドの残りをそのタスクの継続として登録し、それから、呼び出し側にすぐに戻れ;タスクが完了するとき、タスクは継続を実施する。」だ。

不運にも、asyncとawaitという文脈上のキーワードが意味するものに関して、最初に提示された人々の直観力は、しばしばそれらの実際の意味の正反対である。より良いキーワードを考え出す我々の多くの試みは、より良いものを見つけることができなかった。あなたが、短くてスマートで正しい考えにつながるようなキーワード、またはキーワードの組合せについてのアイデアを持っているならば、私はそれらの話を聞ければ嬉しい。我々がすでに持っていて、いろいろな理由のために拒絶したいくつかの考えは、以下の通りだった:

wait for FetchAsync(…)
yield with FetchAsync(…)
yield FetchAsync(…)
while away the time FetchAsync(…)
hearken unto FetchAsync(…)
for sooth Romeo wherefore art thou FetchAsync(…)

冗談はさておき。我々は、おおうべき多くの地面を得た。私が次に話したいのはそれである;「私がこの前適当にごまかしたそれらの「アレ」とは、正確に何であるか?」

この前、私はC# 5.0の式

document = await FetchAsync(urls[i])

は、以下として理解されることをほのめかした。

state = State.AfterFetch;
fetchThingy = FetchAsync(urls[i]);
if (fetchThingy.SetContinuation(archiveDocuments))
  return;
AfterFetch: ;
document = fetchThingy.GetValue();

アレとは、何であるか?

非同時性の我々のモデルにおいて、非同期メソッドは、一般的にTask<T>を返す;FetchAsyncがTask<Document>を返すと今のところ仮定しよう。(また、私は後日このタスク・ベースのAsynchrony Patternの背後の理由を検討する。) 実際のコードは、以下として実現される。

fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
state = State.AfterFetch;
if (fetchAwaiter.BeginAwait(archiveDocuments))
  return;
AfterFetch: ;
document = fetchAwaiter.EndAwait();

FetchAsyncへの呼び出しは、Task<Document> ――つまり、「熱い」実行タスクを意味するオブジェクト―― を生み出して、返す。このメソッドを呼ぶことはすぐにTask<Document>を返す。そして、それはその場合どういうわけか望ましい文書を非同期で取ってくる。それはおそらく別スレッドの上で動くとか、それはおそらくスレッドのWindowsメッセージ・キューのようなものにそれ自体をポストして、メッセージ・ループ的なものはアイドル時に、される必要がある仕事に関する情報についてのポーリングをしているとか、そんなところだろう。それはそいつの仕事である。我々が知っているものは、それが完了するとき何かが起こることが必要だということである。(また、私は後日シングルスレッドの非同期性を検討する。)

それが完了するとき、何かを起こらせるために、我々はタスクにAwaiterを要求する。そして、それは2つのメソッドを公開する。BeginAwaitは、このタスクに継続を登録する;タスクが完了するとき、奇跡は起こる:どういうわけか、継続は呼ばれる。(繰り返すが、これが正確に統制される方法は、後日の話題である。) BeginAwaitがtrueを返すならば継続は呼ばれる;もしそうでなければ、その場合、それはタスクがすでに完了したからなので、継続メカニズムを使う必要がない。

EndAwaitは、完了したタスクの結果を抽出する。

我々は、Task(論理的にvoidを返すタスクのために)、および、Task<T>(値を返すタスクのために)の上でBeginAwaitとEndAwaitの実装を提供する。しかし、TaskまたはTask<T>オブジェクトを返さない非同期メソッドはどうなのか?そこでは、我々は我々がLINQのために使用した同じ戦略を使用するつもりである。LINQでは、あなたがこう言ったならば、

from c in customers where c.City == "London" blah blah blah 

その場合、それはこのように翻訳される。

customers.Where(c=>c.City=="London") ……

そして、オーバーロード解決は、最高のWhereメソッドを見つけようとする。顧客がそのようなメソッドを実装するかどうかをチェックすることによって、あるいは、実装していなければ、拡張メソッドへ行くことによって。GetAwaiter / BeginAwait / EndAwaitパターンも同様である;我々はまさに変化した式でオーバーロード解決をして、それが何に行き当たるかについて見る。我々が拡張メソッドへ行く必要があるならば、我々はそうする。

最後に:なぜ「Task」か?

ここでの洞察は、非同期は平行を必要としないが、平行は非同期を必要とするということと、平行に役立つツールの多くが、非平行の非同期のために同じように簡単に使われることができるということである。Taskには固有の平行性がない;Task Parallel Libraryが、平行化できる保留中の仕事の単位を代表するタスク・ベースのパターンを使うことには、マルチスレッディングを必要としない。

私が2、3回指摘したように、結果を待っているコードの観点からは、その結果がこのスレッドのアイドル時に計算されているとか、このプロセスのワーカー・スレッドでとか、このマシンの別プロセスでとか、ストレージ装置でとか、世界の裏側のマシンでとか、そういうことはまったく重要でない。重要なことは、結果を計算するのに時間がかかりそうで、我々がそのようにしさえすれば、このCPUはそれを待つ間に他に何かしていることができたということだけである。

TPLからのTaskクラスは、すでに多くの投資をうけている;それは、キャンセル・メカニズムなどの役に立つ機能を得ている。新しい「IFuture」型みたいに、何か新しいものを発明するよりはむしろ、我々は単に既存のタスク・ベースのコードを拡張するだけで、我々の非同期ニーズを満たすことができる。

次回:さらなる非同期タスクを組み立てる方法。


インデックスへ戻る


Last Update: 2012-09-26 09:35:24