async/await不要論
2013/3/23
bleis-tif t
自己紹介
いつの間にか発表者にされていた系
仕事ではきょんくんをいじって遊んでます
なんか発表者にされていたので、仕事中にきょんくんをいじっていたらきょんくんが
 ロックフリーの発表をすることになりました
基礎の話とかできないので、基礎勉強会なのに応用よりの話をします
Microsoft MVP for Visual F# を受賞してますが、今日はC#の話が中心です
async/await不要論・・・の前に
昔話をします
初めて買ったPCのCPUは Duron 850MHz (2000年ごろ)
ハイエンドCPUが、ちょうど1GHzを越えたあたり
 ◦ PentiumⅢ1GHzとか、Athlon 1.2HGzとか

このころは、「2010年には20GHzのCPUを実現」とか言ってた
 ◦ 同じように、Hyper-Threadingやマルチコアの「サーバ用途での」重要性も言われ始めた

Intelはこの頃デスクトップ向けCPUでクロック周波数を向上させ続けていた
 ◦ プログラマにとっては、「フリーランチの時代」

しかし、2003年にクロック周波数の向上ペースは落ちてしまった
 ◦ 増え続ける発熱に対処できなくなった
 ◦ 2013年3月現在、x86向けCPUでの最高クロック周波数は4.2GHz
 ◦ フリーランチ時代の終焉・・・?
CPUはデュアルコアへ
2005年に、Pentium4のダイを2つ搭載したPentium Dや、Athlon64のデュアルコア
版であるAthlon64 X2が登場
この頃から、「時代はマルチコア、メニーコアだ」と言われ始める
 ◦ 「2015年にはコアの数は100以上になる」

しかし、未だにデスクトップ向けCPUのコア数は最大8
 ◦ メーカー製PCはいまだに4コアどころか、2コアのものも珍しくはない
 ◦ 「CPUメーカーはコア数を増やす方向にシフトした」のような記述は、半分正解で半分間違っ
   ているので、そういう記事・書籍は眉に唾をつけて読むこと




   クロック周波数も頭打ち、メニーコアも当分実現しそうにない・・・
   CPUメーカーの怠慢?
いやいや
クロック周波数だけが性能を表すわけではない
◦ クロック周波数では測れない部分で、シングルスレッド性能は伸び続けている
◦ フリーランチはそれなりに継続中

一般的なPCの用途にメニーコアが必要とされていない
◦ 現状、最重要なのはいまだにシングルスレッド性能
◦ コアを増やしてもシングルスレッド性能は向上しない
アムダールの法則
                                          15%




            並列化
並列化できない処理   できる
            処理




                                    50%
                                                60%




                  並列化
                  できない   並列化できる処理
                   処理




                                                  50%
現状の把握
並列化できない処理が支配的な現状、
マルチコアCPU・メニーコアCPUは作ったとしても買ってもらえない
◦ デスクトップPCではマルチコアCPU・メニーコアCPUへの移行は当分先(移行しない可能性も)

これからは並列プログラミングが重要になる!という風潮について
◦ サーバサイドではそれこそ10年以上前から重要だった
◦ クライアントサイドではキラーアプリが作れていない現状、それほど重要ではないのでは?
 ◦ キラーアプリ作るのが使命なんだ!って人→応援したい
 ◦ 単純に楽しい!って人→すばらしい思う
 ◦ 将来のためにやっておこう、って人→いいと思う
 ◦ なんかそういう風潮だから・・・って人→いったん、立ち止まってみてもよいのでは?

「とはいっても、C#5.0から言語仕様にasync/awaitが追加されてますよ?」
◦ よし、ではそのあたりから話をしよう
今日話すこと
並列プログラミングと非同期プログラミング
async/await以前の非同期プログラミング
async/awaitの登場
async/await不要論
async/await必要論
そしてF#へ
並列プログラミングと
非同期プログラミング
マルチスレッド化の目的
マルチスレッド化する目的は、大きく分けて2つある
1. 複数のコアを活用し、処理を高速化する(並列プログラミング)
2. 処理をブロックさせないように、バックグラウンドで重い処理を走らせる
   (非同期プログラミング)

前者は複数のコアがなければ意味がないが、後者は単一コアであっても意味がある
◦ ○ CPUがマルチコア・メニーコアに向かうから並列プログラミングは重要である
◦ × CPUがマルチコア・メニーコアに向かうから非同期プログラミングは重要である
◦ 非同期プログラミングの重要性を説くためにマルチコア化の流れを持ち出すのはおかしい




   今日は、並列プログラミングではなく、非同期プログラミングの話をします
非同期プログラミング
非同期プログラミングにも、大きく分けて2つの目的がある
1. 重い処理をバックグラウンドで走らせる
2. 非同期I/OによってI/Oの待ち時間を隠す

今日はこれらを実装する側というよりも、使う側視点
◦ なのでどっちも同じようなものとして扱います

非同期コードを束ねたものを複数コアCPUで実行すると、並列動作する
◦ これを並列プログラミングと言っていいのかは・・・わかりません><
async/await以前の
非同期プログラミング
.NET Frameworkにおける
非同期プログラミングの移り変わり

                                                          4.5

                                         4.0

                                                                     TAP + async/await構文
                          2.0                  TaskによるTAP
                                               (Task-based Asynchronous Pattern)

                                完了イベントによるEAP
                                (Event-based Asynchronous Pattern)
.NET Framework 1.1
                     Begin/EndによるAPM
                     (Asynchronous Programming Model)
各手法の比較のためのお題
2つのWebサービスから情報を取ってきて表示する
 ◦ ただし、WebサービスBから情報を取ってくるには、WebサービスAから取得した情報が必要

これを同期プログラミングで書くとこんな感じ




でもこれだと、PrintSomeData の処理が終わるまで、
 呼び出し元をブロックしてしまう
GetA や GetB を、AMP/EAP/TAPそれぞれで非同期にした場合に
 PrintSomeData がどうなるかを見てみましょう
APM(非同期プログラミングモデル)
BeginHoge メソッドに完了後の処理的なものを渡すスタイル


                            これでもだいぶマシになった方で、
                     .NET Framework1.1時代はラムダ式も無名デリゲー
                       トもなかったので、これよりももっと地獄だった




コールバック地獄
Begin に渡すのは完了後の処理ではない。対応する End を呼び出して完了する
コールバックの引数を、正しく対応する End に渡す必要がある
Begin に対応する End を呼ばないと、リソースリークの原因になる
EAP(イベントベースの非同期プログラミングのデザインパターン)
完了イベントに完了後の処理を登録するスタイル

                     ここには出てこないけど、GetAAsyncとかの実装側
                        はそれはもう大変なコードが必要だった




やっぱりコールバック地獄
HogeAsync を呼び出す前に完了後の処理を登録する必要があるため、
 処理の記述の順番と実際の処理の順番が逆転している
えっ、これAMPの方がマシじゃね・・・?
TAP(タスクベースの非同期パターン)
メソッドに完了後の処理を渡すスタイル

                      GetAAsyncの実装はタスクをStartNewするだけなので、
                            実装する側も使う側も労力が一番少ない
                       (完了後の処理をGetAAsyncの引数としてではなく、
                       Taskクラスのメソッドに渡すようにしたのがGood)




結局コールバック地獄
でも一番すっきりしててわかりやすい
各手法の比較
どれもコールバック地獄
APMは、ジェネリクスがなかった時代に戻り値の型を厳密に指定するために
 ああいうAPIになった。許す。
TAPは、ジェネリクスがある時代に作られた、APMの正当進化系。良い。
EAPは・・・どうしてそうなった。
同期版とTAP版を比べてみる




 同期版を機械的に変換して、非同期版が作れそう・・・!
async/awaitの登場
async/await
APMにしてもEAPにしてもTAPにしても、コールバックの嵐だった
コールバックによるプログラミングはネストも深くなり、見通しが悪い
C#5.0から実装された async/await を使うと、同期版のように書かれたコードを
 コンパイラが非同期版に変換してくれる




コールバック地獄からの解放
◦ 「垂直落下的なコード」て表現されてることもあるとおり、読みにくくない
◦ 逆に、これでどこがどう非同期で動くのかイメージしにくいかも?
同期版と
TAP+async/await版を比べてみる




ほとんど同じ
◦ メソッド名の語尾がAsyncに(そういう慣例・規約)
◦ メソッドの先頭に async がついている
◦ GetAAsync/GetBAsync の呼び出しの前に await がついている

async/await、素晴らしいじゃないか・・・
async/await不要論
async/await再考(not 最高)
async/await は確かに便利だけど、結局やっていることは構文の変換
◦ ラムダ式のネストをフラットにしているだけ(実際は違うんだけど)

構文の変換といえば、C#にはクエリ式があるじゃない!




クエリ式で書けそうじゃないですか?
クエリ式
その名の通り、何らかのデータに対する「問い合わせ(クエリ)」を
 言語組み込みの構文として用意したもの




その名に反して、規定されたシグネチャに適合すれば
 データに対する問い合わせである必要はない
◦ 以下の2つの関数があれば M<T> 型の式をクエリ式に組み込める
 ◦ M<T> 型の m と、Func<T, U> 型の f に対して、m.Select(f) の形で呼び出せる関数
 ◦ M<T> 型の m と、Func<T, M<U>> 型の f と、Func<T, U, V> 型の g に対して、
   m.SelectMany(f, g) の形で呼び出せる関数
拡張メソッドで2つの関数を実装
Task<T> にインスタンスメソッドを後付けすることはできないので、拡張メソッドで




                        ContinueWith は F<Task<T>, U> を受け取って
                          Task<U> を返すので、U が Task<V> だと、
                               Task<Task<V>> になってしまう



             Unwrap が Task<Task<T>> を Task<T> にしてくれる


効率とか異常系とか細かいことは、今回はパスで
SelectManyは元のコードの構造を
写し取ったような構造をしている
クエリ式で書いてみる



async/await と同じことができた!
async/await 版との違いは
 ◦ 通常のC#の文法ではなく、クエリ構文を使う
 ◦ 最後に全体を変数に格納する必要があるConsole.WriteLine が直接使えない
   (let で受けるか、上のように非同期化する)
 ◦ async が不要
 ◦ 何か特殊なことをやっていると分かりやすい
さらに
APMをクエリ式で直接扱うようなSelect/SelectManyを実装できる
 ◦ APMをTAPに変換するのではなく、APMをAPMとしてクエリ式で扱える

EAPもおそらくクエリ式で直接扱える・・・んじゃないかなぁ
 ◦ できなくてもTAPに変換すればTAPとしてクエリ式で扱える




          すべての非同期モデルを、
       クエリ式という統一した構文で扱える!

                        async/awaitなんていらんかったんや・・・
async/await必要論
クエリ構文 VS async/await構文
                        クエリ構文       async/await構文
 メソッドの制約      なし                asyncをつけ、戻り値も制限
 式全体の型        Task<T>           T
 通常の構文との差     大                 小
 対応している制御構文   逐次と、条件演算子による分岐    逐次、分岐、繰り返し


async/await は大体の制御構文と混ぜて記述できる(しかも型がT)のが大きい




クエリ構文でこれは厳しい・・・
やっぱりasync/awaitだよね!
だがしかし。




            catchで使えない!




             finallyで使えない!
C#さんは
「こういう便利な機能追加したんですよ!」
「へぇ、すごい!」
「でもここでは使えないんですけどね!」
っての多すぎやしませんか!!!
readonly で読み取り専用にできるけど、フィールドでしか使えない
var で型を省略できるけど、ローカル変数でしか使えない
yield で簡単に IEnumerable<T> 作れるけど、ラムダ式の中では使えない
async/await で簡単に非同期処理書けるけど、catch の中では使えない←New!
個人的にはですね
クエリ式にもっと頑張ってもらいたかった
◦ 名前からして、クエリ専用なことが明らか
◦ だけど仕組みはものすごい汎用性がある
◦ SQLみたいな感じだよー!とするにはつらい(JOINとかな!)

無理なものは仕方がない
◦ そこでF#ですよ!
そしてF#へ
F#のコンピュテーション式
F#には、C#のクエリ式のように「構文を変換する仕組み」として、
 コンピュテーション式というものがある
「コンピュテーション(計算)」とあるように、とても汎用的な仕組み
F#標準で、非同期ワークフローが扱える
コンピュテーション式とクエリ式
             コンピュテーション式            クエリ式
    目的   計算一般                クエリ
    構文   他の構文に近い             他の構文とは全く別
    制御構文 順次、分岐、反復などに対応可能     順次と(制限のある)分岐のみ


他にも、F#は同名の変数により変数をシャドーイング(隠蔽)したり、アンダースコア
 によって値を捨てたりできる

.NET Framework2.0移行であればF#の基本機能はすべて使える
例外処理中の非同期処理だって
C#は出来なかったけど、F#ならできちゃう
それぞれの使い分け
F#が使えるのであれば、F#を使う(.NET Frameworkのバージョンは問わない)
そうではなく、C#5.0/.NET4.0以降が使えるのであれば、async/awaitを使う
そうではなく、Taskが使えるのであれば、TAP + クエリ式を使う(RXでも可?)
そうではなく、C#3.0が使えるのであれば、APM + クエリ式を使う
そうではなく、C#2.0が使えるのであれば、APMを無名デリゲートとともに使う
C#1.2しか使えないのであれば、APMをメソッドとともに使うしか選択肢はない
EAPは基本、使わない
さいごに
async/awaitの潜在能力?
async/await は調査が全然足りてない
もっと面白いことができる可能性もある
 ◦ async/awaitも、クエリ式同様「こういうシグネチャを持っていればいい」系
 ◦ クエリ式をクエリ以外に使えるように、async/awaitも非同期処理以外に使えるかも・・・?

そんなことしてると「ふつうのC#プログラム」からどんどん離れていきますけどね
最後にこれだけは言いたい


クエリ式はモナド用の構文!
 異論は認めない!!!

async/await不要論