matarillo.com

The best days are ahead of us.

C# 5の非同期、パート5: タスク大杉

2018-01-09 10:24:18

原文

都市にはたくさんの銀行支店があり、その支店にはたくさんの金銭出納係と1人の雑用係がいるとしよう。たくさんの顧客が都市にいて、それぞれは一日の様々な時刻に銀行からたくさんのお金を引き出したい。

アルゴリズムは、このように動作する:

顧客は最も近い銀行支店を見つけて、その行列を評価する。行列がドアから出ているならば、顧客は別の銀行支店へ行く。これは、彼らが十分短い行列をもつ支店を見つけるか、あるいは、彼らがあきらめて家に帰るかするまで続けられる。

彼らが十分短い行列をもつ支店を見つけるとしよう。顧客は行列に並ぶ。(おそらく、彼らはMモデルを使うかWモデルを使うかして行列に並ぶ; それはこの類推の目的において特に重要でない。それがWモデルであるとしよう。) 顧客が金銭出納係に到達するとき、トランザクションはこのように進行する:顧客はいろいろな単位の特定の数の紙幣を要請する、そして、金銭出納係はそれらを数えあげる:一枚、二枚、三枚、四枚、五枚、はいどうぞ。顧客は去る、そして、金銭出納係は次の顧客にサービスする。

これは、全く合理的なようである。しかし、金銭出納係のトランザクションの途中で正しい現金の単位が尽きるとしよう。金銭出納係は、金庫室に行ってあと50枚の紙幣を取ってくるように雑用係に言って、それから、雑用係が金庫室に走って行って戻ってくるまで少しの居眠りをとる。現在、顧客は、そして、 彼らの後のあらゆる顧客 は金銭出納係が起きるのを待たなければならない。そして、それは雑用係が金庫室から戻るまで起こらない。

その話がわかりやすくないといけないので、この類似において、都市はサーバー・ファームであり、銀行支店はマシンであり、金銭出納係はワーカー・スレッドであり、雑用係はI/O完了スレッドであり、そして顧客はクライアントである。お金とはサーバーがクライアントに代わって実行する計算であり、雑用係が金庫室に行くのを待つことはI/O完了を待っている同期の遅れである。支店を選ぶには行列があまりに長いかどうか決めることは、サーバー・ファームのロード・バランスをとることである。

現在、これがより効率的でありえたとあなたは言うかもしれない。金銭出納係が雑用係を待ちながら眠っている時間は、トラブルの元である。理想的には、あなたは常に全力を尽くしているコアにつき、1つのスレッドを望む。金銭出納係は行列の次の顧客にサービスして、それから、雑用係が戻るときに最初の顧客を引き取ることができた。これは、タスク・ベースの非同期の理想的な使用のようである。

あなたがこのように変更をするとき、全部のシステム上で影響を理解するように、あなたは本当に注意しなければならない。偶然に以下のシステムを実装することは、本当に簡単である:

顧客は、最も近い銀行支店を見つける。行列がないので、彼らはすぐに入る。金銭出納係がいない。「数字を受け取ってください」という機械とドアを指している矢だけがある。顧客は数字を受け取って、ドアを通って歩き、何万もの顧客を含んでいる 巨大な倉庫 に着く。最高の速度で顧客から顧客まで走りながら、顧客に各々の 紙幣を一度に一枚 与えているたくさんの金銭出納係がいる。金銭出納係が必要とされた単位が不足するならば、彼らは雑用係にどなって、すぐに彼らが応対をすることができる次の顧客に進行を続ける。そのうち、雑用係は彼らに必要とされた単位を持ってくる。金銭出納係は、自分がいったい誰に「同時に」サービスしているか経過を追っており、そして、顧客は彼らのすべてのお金を得るまで誰も去らない。雑用係がついていくことができないならば、すぐに倉庫はますます込むようになり、より込んでいるようになるほど、すべてのお金を得ようとする顧客は時間がかかり、彼らが待つ間により多くの顧客が到着し、まさに雪だるま式に増大する。

このスキームでは、金銭出納係はほとんど眠っていないので、CPUコアはかなり利用されており、顧客の誰もが最初の紙幣をかなり速く受け取る。「結果の最初のバイトまでの時間」は、潜在的に素晴らしい。しかし、ロード・バランシングはまさに完全に消えてなくなった; 決して行列がないので、応待されてない顧客が多すぎることをロード・バランサーが知る方法がない。そして、供給される最後のバイトまでの時間は、潜在的に悪くありえる。

これは愚かな物語のように聞こえるが、我々自身、ちょうど先日偶然にこのようなことをした。我々は、UIスレッドの上でタスク・ベースの非同期を使うコード・アナライザーを構築した。着想は、あらゆるキーストロークに関して我々が非同期タスクを始め、それからそのタスク自身が他の非同期タスクを始めるということであった。タスクは以下の通りである:

  1. 続くキーストロークが最近あったかどうかを知るためにチェックする; もしそうならば、それから、このキーストロークに関連する未完了のタスクはすべてキャンセルする。
  2. キーストロークによって引き起こされる構文木の変更の差分分析に基づき、ユーザ・インタフェース・テキストを再度色づける。
  3. タイマーをセットして、半秒の間キーストロークがなかったかを知る。もしそうならば、ユーザーはタイプを休止したかもしれないので、我々はより深いプログラム分析を実行するバックグラウンド・ワーカーを起動できる。

問題がわかるだろうか?キーストロークの間にタスクは とてもすばやく つくられるので、あなたが速くタイプしているならば、すぐに、何万ものタスクでいっぱいの倉庫が残ることになり、それの99.99%はまだ自分自身をキャンセルするための動作をしていない。そして、なんとか動作することができたタスクの半分はタイマーを作成していて、膨大な大多数は少しでも動く前に削除されようとしている。すべてのそれらのタイマーだけから来るガーベージ・コレクションの圧力は、パフォーマンスを無効にするのに十分だった。非同期はすごいが、あなたは非同期の粒度が適切なレベルであることを確認する必要がある。コード・アナライザーは、一つのグローバルなタイマーを参照したタスクを待ち行列に追加して、0.1秒後にキャンセルを必要としそうなタスクについては積極的に待ち行列に追加しないように書き直された。パフォーマンスは大幅によくなった。

タスク・ベースの非同期はすごいが、それは万能薬でない; 進む時には慎重になり、そして測定すること。あなたが要求をする時間と、非同期要求がタスク・キューへ自分自身を追加する時間の間で、少しも待つことがないという事実は、あなたが速くつくることができるタスクの数に対して少しの規制もないことを意味する。

次回: 構文上の関心事に関するより多くの考え。


インデックスへ戻る