rider.お品書きサクッと書いて公開して、後でちまちま修正する事がママあります。なんじゃこりゃ?と思ったら、数日してから見て貰えると嬉しいです。「スクラムの実践方法を分かりやすくマンガも交えて解説。AICHI”を開催・登壇したので、その話をまとめます。Coreの非同期処理”最初の方はキーノートのまとめ的なものにしようと思っていたのですが、その話はまた後で。8.0で.NETの非同期処理が一応の完成を見たと思っているので、改めて全体を網羅するような内容にしようと思ったものです。非同期処理の誤用が話題に登ったり、辛しみのあるコードを目撃することから「こんな」記事を書いたりしていたのですが、もっと網羅的にまとめたいなーと思っていたので良い機会として共有しようというところを目標にしました。(SlideShare)セッション50分でこの分量は無理ゲーなのはわかっていたので、スライドは後で読み返すことが出来るような構成にして、如何に喋りで補えるかに注意しました。セッションを聞きながら見ていない場合は、少し足りない(わかりにくい)部分があるような気はしています。セッションビデオと両方で補完してもらえればと思います。最近twitter上で非同期の事について話ししたものをいくつか置いておきます。みたいなところから、ちょっとずつネタを拝借。普段、この方面の話は書かないのですが、楽屋ネタを書きます。Hanselmanが、日本のコミュニティ向けに何か喋ってくれるかもしれない、という話がもちあがり、それならその方向から構成を考えるために、決定するまで待つかと思っていました。みなさんご存知の通り、大変に忙しいキーパーソンなので、この試みがキャンセルされるという事態も考えられましたが、関係者が頑張って調整に入っているようなので、ギリギリまで待つつもりでした。運営は本当に頭の痛い問題なのですが(これで折れてしまう方もいるので、私は自分が出来る範囲で出来ることしかやらない事を守るスタンスです)、登壇する側としても、セッションネタを考える場合に、上記の制限条件からネタ構成を考える必要も出てきます。10名・30名・50名・100名かそれ以上、によって、セッションの構成(しゃべる内容や構成、進行方法)を変えたほうが良い。人数が多くなると、後ろの方の席の人は、スライドが読みにくいだとか、声が聞こえないとか、没入感が得られない、といった潜在的な問題が発生しそう。スライドの色校正(たいてい黒地だとハマる会場は多いが、パリッとした映像を出せる会場なら、逆に黒地が映える。動画を流す場合は音量に注意しないと、うるさいとか音が小さいとかの問題につながる。参加人数が多いと、特別に設計されたWiFiアンテナなどを備えた会場でないと、話にならない事態になります。私はあまりオンライン向きのネタを扱わないので良いのですが、最近だとクラウドアクセスをデモの前提にする必要があったりして、非常にやりにくいでしょうね。逆に、ネットワークの信頼性が高い事が前提にできるなら、クラウドだろうがYouTubeだろうが、セッションの子道具としてバリバリ使えます。細かいところを挙げるとキリがないのでこのぐらいで。(人にもよりますが)、セッションネタ一つ決めるのにも、こういう要素が絡んできます。登壇する側は事前にこれらの要素が早く確定していれば、それだけ登壇者や参加者にとって、実りのある内容に詰めていける可能性があるわけです。冒頭の話につながるのですが、今回は会場を日立ソリューションズさん(HISOLさん)にお借り出来たのですが、その前にもいくつか候補があって調整していました。全ての会場が完全に同一条件なら、「まあ確保できるところでやればいいよね」と、問題の一つが片付くのですが、現実はそうじゃないのでヤキモキします。結局、検討していた会場は確保できなかったのでHISOLさんに打診したところ、快諾得られたので良かったのですが、確定までに2週間前までかかったこともあって、なかなか会場を公表できませんでした。登壇者のみなさんごめんなさい。いう点で、名古屋は非常に苦労するというのはなかなか理解してもらえないところでツライです。東京に近いところは楽そうだなと思うし、もっと地方ではそもそも会場になる場所さえ無く、勉強会というものを想像するのも難しいと言うのが現実でしょう。問題は上記の会場環境の話に繋がります。HISOLさんの会場では、残念ながらパブリックなWiFi設備を借りる事が出来ません(もちろん有線LANも)。すると例えば、クラウドベースのデモとかかなり厳しい(デモの悪魔に取り憑かれる可能性が高い)わけです。これはイベントの一体感もあるので、ある程度の規模のイベントではやりたくなることの一つです。実際、もしかしたらHanselmanさんとTeamsで完全オンラインでの相互コミュニケーション、というのも検討の一つとしてありましたが、まあ会場的に無理なので、実現するとなったら有償で会場を確保するかということも考えなければなりません。(そうすると参加費500円とかでは無理ですし、そもそもそんなギリギリで会場を予約できるか怪しいし、堅苦しい契約書をかわさなければならないですよ、個人で。請求や領収、今だと税金をどうするかみたいな話も出てくるかも)なので、主催が本当に小規模な勉強会を小規模な会場でやる分には良いんですが、ちょっとでも規模が大きくなり始めると、非常に辛くなります。そこを踏ん張ってやるからには、それを超える動機づけが必要になるでしょう。私の場合は自分の技術的な知見をシェアする、まあ淡良くば技術ポートフォリオとして誰かの目に止まるかどうか、ぐらいで、これで食っていく気は全く無い(食える気がしない)ので、必然的に掛けられる労力もそのぐらいになります。いうわけで、様々な軸での考えがあるため、これらをどこにバランスさせて、セッションのネタにするのか、というのは、真面目に考えるととんでもなく難しいです。Confを拾い上げるにしても、様々なセッションから何をネタにするのかは、上で挙げたような要素が、特に会場確保によって大きく変わるので、まずはどこの会場が使えるのかが最大の課題。第一としていた候補はダメ。あと2~3考えていたが、それらはどれも環境的に厳し目なため(WiFiが無いとか、ちょっと会場の母体の趣旨と合わないとか)、この時点でセッション内容についてはかなり幅が狭くなる。Hanselmanさんがビデオメッセージを送ってくれる(あるいはリアルタイムで!)かも??という打診すごい話だけど、運営側としては、完全に確定だからあとはやれるように組む、というのと違い、不確定だとどう動けば良いのかわからずツライ(それもギョムの合間を縫って行動する必要があるので)。早く...別の会場を打診アテの一つは早々に辞退(上記の後者側)、もう一つを打診。Confのセッションを拾い上げるという案は、会場の自由度があるから考えられる手法であって、もうそれは無理なので、結局自分でネタをひねり出すしか無い。非同期の話をしたいから、まあ2枠って事になるかな。問題はもうほかの登壇者さんの募集とか打診とかして枠が埋まりつつ合ったので、もしHanselmanさんの話が現実化する場合、どこの枠(時間帯)をその話にするのかを考える必要がある。Hanselmanの生の話が聞けるなら、みたいな期待で参加者が押し寄せて、出来ませんでしたテヘッ、みたいな事態を想定しなきゃいかんのか、とか...非常にツライ...正直なところは、予定が決まらなかったら、こちらから辞退することも視野に入れていました。自分のネタを考えているはずが、全体の進行まで気になり始めて、それどころじゃない感じ。最悪、キャンセルになって自分で全部仕切る(つまり2枠考えて喋る)ということを想定して準備。なんか色々頑張っても無駄に終わりそうで、それもつらい。Confのキーノートを読み込んで概要にまとめて、面白そうなトピックを実演を交えて喋る、みたいに構成するしかないかな(もはやキャパオーバーだけど、主催自分だから仕方ない)。3日前の夜に、Hanselmanさんのビデオが送られてきた!!もうこの時点では、8割方断る方向で心中決まっていて(でないと間に合わない)、手間が空いたときに断りのメッセージを送るしか無いか、と、散々たる気持ちでいたのですが、こっちからメッセージを送る前に向こうから来た。VideoIndexerで字幕データを作り、それを補正(わりと全自動では駄目な感じの字幕なので、手修正する)して、ffmpegで合成して英語版字幕付きビデオを作る(成果物はここ、但しビデオそのものは含まれていません。時間の関係で、訳を直したりタイムラインを調整したりと言ったところはまだ細部が荒いです。関係者はエンハンスして欲しいです、是非に)。2日前ビデオの内容を読み込み、字幕がアレだったことから日本語字幕化する事を考え始めた。Indexerで日本語訳出せばいいじゃん、と思うかもしれません。機能的にはありますが、まあ、察してください...)それと同時に、セッションをどう進行するかを考えていたけど、ビデオ流しながら補足を入れつつ、(50分枠なので)後半は小さいコードをライブで何かデモするか、そうだねぇ、あんまりどこにも話題にかかっていないgRPCとか良いかもしれない、と考えて、概要を調べる。自分でパース出来るのでは?と思いつく。APIを使って日本語に翻訳(4G経由の通信なので、ここにリスクはあるが)して日本語字幕ファイルを出力するツールを書き、ffmpegで合成して、日本語字幕のついたビデオをその場でみんなに見てもらう、というのは、かなりエキサイティングじゃないか?というのを思いついたので、パーサーを書き始める。パーサーは簡単だった。3としてもふさわしい。API使っているのかって、自問してましたよ...しかしここで字幕特有の難しさにブチ当たる。それは、1センテンス(日本語なら読点まで)が必ず1画面に収まるわけではなく、分割されたセンテンスに推測不能(一定時間ではない)時間タグがついて分割されている、ということなのです。Matsui.”“私の名前は松井幸治です。46/54なら、「は」の後ろで丁度切れるかもしれない。net20とか古すぎる。NMeCabをベースにチャッチャと移植して、これはこれで別の機会にネタとしてまとめよう、と思いついて移植を始める。netstandard2.1対応のNuGetパッケージ、しかも標準のIPADIC同梱でありながら、自分の辞書を使う事も出来るように構成可能にして公開しました。寝る時間が無くなって本番の運営に支障をきたしそうなので、無念だけどここでピボットする事に。MeCab.DotNetのことと日本語字幕ツールの話は、また後日のネタにする事にして、当日はHanselmanの英語字幕付きビデオを流してコメントを入れつつ、その後にHanselmanのデモと同じことを私が再現して、多分言い足りない部分を補う感じにすれば、丁度50分に収まるだろうと考えました。(今回のセッションではなく)不特定の人に英語技術ビデオを見た後に感想を聞くと、なんか腑に落ちない顔をしていることがあります。英語や英語の字幕のせいでピンときていないのかも知れないし、ビデオでは簡単にやって見せているけど、周到に準備してるからじゃないの?というような疑念があるんじゃないかと思っているので、同じことを目の前でやって見せることで払拭したかったのです。(Hanselmanさんのデモ自体は、上記のような不安要素は殆どなくて、押さえるべきところが押さえられていて流石だなと思っています。)ポイントはそんなに多くなくて、実際やって見せればシンプルな話だと思っていて、目の前で動いているプロセスの状態を見たりとか、今まさにWindows側でビルドしたコードが、WSLのubuntu上でそのまま動きましたよね(間違いなく)、みたいなことじゃないかなと。いうわけで、これで50分のセッションを構成することにしました。この内容の利点は、資料が一切不要なこと。自分の喋りと進行をその場で工夫して乗り切ればOKだ。この時点でもう23時なので、他の手段は取れそうにない...妄想ということで、今回のイベントの裏がどうなっていたのかが何となくわかっていただけたかと思います。半分は運営全体の話、半分は個人的な事情によるものですが、こうしてみるとやっぱり運営に割ける時間が足りない、運営に手間を取られすぎる、ということが大きかったと思います。コミュニティへの貢献は色んな方法が考えられます。私が一番して欲しいと思っていることは、ぜひ、自分の時事ネタでも興味のあることや知見でもなんでも良いので、発表・シェアしてほしいです。コミュニティに物理的な(生々しい話をするなら金銭的な)貢献をして頂ける場合は、運営の裏舞台、特に発表とは関係のない諸々の作業(これは私の場合ですが、それぞれのコミュニティの事情を読み取って)の負担を軽減してもらえて、結果的に時間を削減出来ると、本当に助かります。時間を確保出来るようになれば、発表する内容をより吟味したり、面白く楽しい内容に出来るようになり、より良いコミュニティイベントを開催・継続できることにつながると思います。(あえて挙げるとすれば、名古屋の場合はやっぱり会場ですかね。リッチな環境が整備された会場が提供されると、悩みの半分は解決します。場合によっては7割ぐらい楽になる感じ。今や募集とかはconnpassのようなサービスで出来るし、あとは当日そこに行って発表する事を考えるだけなので。岐阜で登壇してきました。「M」とは「モナド」のことです。内容について誰にも推敲依頼しなかったので、実際に発表するまで内容の正確性に自信がなかったのですが、大きな問題はなさそうでホッとしました。最初に喋りましたが、これについては解説の筋道を2年ぐらい考えていました。なんとか形になってよかったのと、自分の口で説明することでより理解が深まったのが大きな収穫でした。帰りにはもみあげさんとコレクションへのモナドの適用(要するにLINQのSelectMany)がモナドであるのか否かの話も出来たので、これも大きかったです。次はモノイドの理解とモナド・モノイドの(頭のなかでの)統合が課題かな。Petricekのときに出た、モナド(又はモノイド)に対するコンピュテーション式の適用での矛盾が元ネタで、この時にはまだ消化しきれなかったので、いつか噛み砕いて理解しようと思っていたことです。このあたりが理解できると、抽象化の道具がまた一つモノになったと言える気がします。今日の発表で、一歩近づいたかな。内容はbleisさんのGistに書いてありますが、今見てもまだ怯んでしまうな...それではまた。ADは既に埋まっていた...)それには、こんな事情があったのです:「.NET非同期処理(async-await)を制御する、様々な方法」の記事がコンスタントにPVを稼いでいる(その割に、他の非同期関連の記事は読まれない)。asyncやawaitキーワードの使い方を度々聞かれる。Task.Run()とか、Task.Factory.StartNew()とか、Task.Start()とか使ってるコードを頻繁に見る(不要なのに)。Task.Wait()とか、Task.Resultとか使ってるコードを頻繁に見る(不要なのに)。その中でTask.Wait()とか(?!?!)私のセッションや記事は、「なぜなのか」を深掘りして問題の根本を理解してもらう事を念頭に置いているのですが、さすがにこれだけ頻出してるのは、業界的に非常に問題があるんじゃないだろうかと思いました。Coreが注目されると、今までC#でコード書いた事が無い、と言う人が、見よう見まねで書き始める時期じゃないかと思います(それで何となく書けてしまうのが、C#の間口の広いところではあるのですが)。必要性」のような記事を書いているのですが、まだ完成していないので、もうちょっとライトなやつを書いてさっさと公開した方が良い気がしてきました。「できる(なんちゃら)」じゃないけど、そういう資料も切り口を丁寧に詰めればありかも知れない...そういうわけで:C#の非同期処理(Taskとasync-await)を使ってコードを書くなら、以下の事を知っていれば9割がたの問題は回避できる!!ここで示した方法から外れたことをしようとしている場合は、殆ど間違いなく誤った記述をしようとしているので、もう一度何をしようとしているのか整理しよう!!(知っててやってる人はいいんです。こんな記事見る必要はありません)章立てを2章だけにして、極限まで絞り込みました。コードはやや冗長に書いています。同期処理と非同期処理を対比させてみる以下のコードは、ウェブサイトから「同期的に」データをダウンロードします。要するに、普通のコードです。ReadFromUrl(url);Console.WriteLine(body);}これを非同期処理として変形します。変形した箇所にはコメントを入れておきました。メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。メソッド内でawaitキーワードを使ったら、戻り値の型はTask<T>型で返さなければならない。T型は本来返すべき戻り値の型。非同期処理対応メソッドは、「慣例」で「~Async」と命名する(慣例なので必須ではない)。OpenReadから非同期対応のOpenReadTaskAsyncに変更する。OpenReadTaskAsyncがTask<Stream>を返すので、awaitする。awaitすると、Streamが得られる。ReadToEndから非同期対応のReadToEndAsyncに変更する。ReadToEndAsyncがTask<string>を返すので、awaitする。awaitすると、stringが得られる。メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。メソッド内でawaitキーワードを使ったら、戻り値の型はTask型で返さなければならない。ここでは元々返す値が無かった(void)なので、非ジェネリックなTask型を返す。非同期処理対応メソッドは、「慣例」で「~Async」と命名する(慣例なので必須ではない)。ReadFromUrlから非同期対応のReadFromUrlAsyncに変更する。ReadFromUrlAsyncがTask<string>を返すので、awaitする。awaitすると、stringが得られる。HttpClientではなくWebClientを使ったのは、対比させやすいためです。非同期前提で書くなら、始めからHttpClientを使うと良いでしょう。WebClientの代わりにHttpClientで同じ処理を書いてみるのは、良い練習になると思います。出来るだけわかりやすくなるようにコメントを入れたので、「ひたすらめんどくさそう」に見えますが、よく見れば大した作業ではない筈です。ReadFromUrlとDownloadのそれぞれのメソッドが、どのように変形されるのかを見てください。すると何となく法則が見えてくると思います。この作業で、2つのメソッド「ReadFromUrl」と「Download」が、両方とも非同期メソッド化された事にも注意してください。特別な理由がない限り、非同期処理は呼び出し元から末端まで、「全て非同期メソッドで実装する必要がある」、と言う事です。ここで示した変形を行うと、連鎖的に、すべてのメソッドが非同期化されます。以下にまとめます:非同期対応メソッドが存在する場合は、そのメソッドを呼び出すように切り替えます。上記では、「OpenRead」を「OpenReadTaskAsync」に変えました。目的の非同期メソッドは、「TaskやTask<T>を返すメソッドがあるかどうか」や「~Async」と命名されているメソッドがあるかどうか、を目印に探すと良いと思います。この理由のために、メソッド名を「~Async」と命名する慣例が生きてきます。TaskやTask<T>は、そのままでは戻り値T型になりません。これらの値は「await」キーワードを使う事で、戻り値を得る事が出来ます。awaitを実行している時、つまりそれが「非同期的に結果を待機している」と言う事です。自分で定義したメソッド内で「await」キーワードを使った場合は、メソッド先頭に「async」キーワードを追加する必要があります。以下のTask型云々は関係ありません。非同期メソッドの戻り値は、Task型、Task<T>型、または「void」とする必要がありますが、voidは殆ど使用しません(次章)。同期メソッドの戻り値の型がstringなら、Task<string>のように、ジェネリックなTask<T>型を使います。同期メソッドの戻り値がない(void)場合は、Task型(非ジェネリック)を返します。英語の動詞変形みたいな話です。この法則だけ覚えていれば、一旦同期的にコードを書いてみて、それから非同期処理に置き換えるという「練習」が出来ます。やっていれば、最初から非同期で全部書けるようになります。これらを使う事はまずありません(断言)。それでも、という方、では第2章へどうぞ。XAMLは省略します。TextBoxコントロールに”textBox”という名前を付けて定義してある前提です(また、本当はデータバインディングでやるべきです)。このコードだと、ダウンロードが完了するまで、UIが固まってしまいますね。メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。メソッド内でawaitキーワードを使ったら、戻り値の型はTask型で返さなければならない。Loadedイベントのシグネチャは変更できない。従って「void」のままとする。非同期処理対応メソッドは、「慣例」で「~Async」と命名する。ここでは「~Async」と命名しない。ReadFromUrlから非同期対応のReadFromUrlAsyncに変更する。ReadFromUrlAsyncがTask<string>を返すので、awaitする。awaitすると、stringが得られる。}}MainWindow_Loadedメソッドと、第1章のDownloadAsyncメソッドとの違いを確かめてください。RoutedEventArgs)」です。戻り値を返さない「void」であることが決められています。非同期メソッドはTask型かTask<T>型を返すべきですが、ここでは型が合いません。値を返さない場合に限り、このような非同期メソッドを「void」で定義します。ポイント:戻り値を返さず「void」と定義するメソッドは、イベントをフックしたハンドラメソッドのような場合しかありません。戻り値をvoidとした場合、呼び出し元は非同期処理の結果を追跡できず、処理結果を感知しません。逆に考えると、Loadedイベントの場合、そもそもイベントの呼び出し元は非同期処理の結果など気にしていません(voidなので)。voidとしても良い、と解釈することもできます。「Task」を返そうが「void」を返そうが、「await」キーワードを使ったので「async」キーワードを追加する必要があります。(何らかの)処理の開始位置が、このハンドラメソッドにある事にも注目してください。このように、呼び出し元(WPFのLoadedイベント処理)が、呼び出し先(MainWindow_Loadedメソッド)の事について感知していないような場合にのみ、呼び出し先の非同期メソッドの実装方法を(voidとすべきかどうか)考える必要があります。DownloadAsync();}第1章のDownloadAsyncメソッドを呼び出してみました。この呼び出しをawaitで待機する場合、Mainメソッドの先頭にasyncと付ける必要があります。このコードは問題なくコンパイルできますが、実行すると(殆どの場合)ダウンロードが完了することなくプログラムが終了します。Mainメソッドは、DownloadAsyncメソッドの非同期処理をawaitで待機します。これは「非同期的に待機」しているので、「Mainメソッドの処理(メインスレッド)は継続的に実行されている」のです。すると、メインスレッドはそのままMainメソッドを抜けてしまい、一瞬でプログラムが終了してしまいます。Task.Wait()を使わざるを得ない。Task.Resultで待機する必要があります。GUIアプリケーション(WPF/UWP/Xamarinなど)を実装している皆さんには、まったく縁のない話なので、このことは忘れても構いません。Task.Resultを使うとデッドロックを起こします。「デッドロックします。いつかTaskに「後ろから刺される」覚悟はしておいた方が良いでしょう...awaitで待機するように記述する事を「大前提」として、そうするにはどうやって書いたらいいかを考えると良いと思います。これだけで、「マトモ」な非同期処理コードを書くことが出来るはずです。繰り返しますが:C#の非同期処理(Taskとasync-await)を使ってコードを書くなら、これらの事を知っていれば9割がたの問題は回避できる!!ここで示した方法から外れたことをしようとしている場合は、殆ど間違いなく誤った記述をしようとしているので、もう一度何をしようとしているのか整理しよう!!これまでの経験で、WPF・UWPなどのGUIアプリケーションで、ここで紹介した方法「以外の」方法がどうしても必要となるパターンは、「一度もありません」でした。Task.Wait()やTask.Resultなどを必要としたケースは、MVVMフレームワークの設計時に、安全な非同期処理を実現するための、ごく内部的なコアロジックを設計したときだけです。これを読んで新たに疑問が湧いてくるかもしれません(例えば、どうしてデッドロックするのか、voidの非同期メソッドの結果はどうなるのか、など)。そのような場合は、より深く解説した記事を読んでみてください。Linking」という題目で、NL名古屋で登壇してきました。NとLの字さえ使っていれば何でも良いというカオスなショートセッション会なのですが、内容は非常に濃くて面白いものばかりでした。満足度が高かったです。Togetterのまとめはこちら今回写真撮り忘れてしまったのでありませんが、50名超えと大入りな感じでした。Asyncのシームレスな相互運用を行うネタを発表してきました。このネタで行くにはまだ「ひよっこ」で恐怖しかない感じでしたが、掴みもスベる事なく発表できたのでうれしかったです。一部の方には触れるものがあったようで、「おおーー!!」という声はやって良かった感ありました。発表はショートセッションですが参加者が多いこともあり、最終的に時間制約が10分となって苦しいところでしたが、予定通り進行を無視して「継続」させていただきましたありがとうございます。補足さて、内容は駆け足だったので少し補足しておきます。FSharp.Control.FusionTasks.FS40検討アイデアはすぐに思いついたものの、実現可能かどうかが良くわかりませんでした。Asyncクラスは似ているな、もしかしたら簡単に相互運用できるかも?と思ったのがきっかけです。2.0から実現している)。TaskはF#に依存しないように改めて設計しなおしたのだと思われます。F#非同期ワークフローは、AsyncBuilderクラスを使用する、一種の構文糖(computation式)だと理解しています。Taskを扱えるのではないかと考えました。Asyncで使うことが面倒であったということがあります。Taskのためのサポートはあります。「Async.AwaitTask関数」がそれですが、この関数は見ての通りT型が特定されなければならず、非ジェネリックTaskをAsync<unit>に変換できません。逆の変換(AsyncからTask)への変換方法もありません。逆の変換ができると、F#で書いた非同期ワークフローのインスタンスを、C#側でawait出来るようになるため、さらに応用性が広がります。computation式については、「詳説コンピュテーション式」が詳しいです。持ち出した「タスクコンテキスト」という用語なのですが、もはや統合されたこの世界では「非同期コンテキスト」としか言いようがないですね。「非同期コンテキスト」をシームレスに結合した場合、維持されなければならない重要なポイントがあります。それは、「同期コンテキスト(SynchronizationContext)」と「キャンセルトークン(CancellationToken)」です。Task側の事はある程度わかっているのですが、F#非同期ワークフローではこれがどのように維持されるのかがわからず、シームレスな相互運用が実現するにはココの担保が不可欠だと考えていました。fsugjp!!)F#にはAsync.FromContinuations<T>関数があり、これを使うとコールバック関数を経由してAsyncクラスの結果を制御できます。これは丁度TaskCompletionSource<T>クラスを使ってTaskを間接的に操作することに相当するので、Taskの結果を反映させることに使えます。余談ですが、初見ではあの引き数で何ができるのか、さっぱりわかりませんでした(つまりビギナー認定w)「Continuations」という単語の響きに導かれ、逆のパターンにはAsync.StartWithContinuations<T>関数を使えばよいことがわかりました。こっちはもっと単純で、正常・異常・キャンセルの継続処理をCPS形式で渡すだけです。同期コンテキスト自体は、Asyncクラス内で非同期待期に使用しているので、F#非同期ワークフローで使う限り特に問題なさそうです。Task.ConfigureAwaitメソッドに相当する同期コンテキストのキャプチャ制御についてはむしろF#の方が柔軟性があり、Async.SwitchToContext・Async.SwitchToThreadPool・Async.SwitchToNewThread関数を使用して、いかようにでも同期コンテキストを操ることができます。キャンセルトークンの方ですが、F#側はAsyncクラスのインスタンスを何らかの方法で実行するときにトークンを保持し、それがAsyncクラスの非同期コンテキストの情報として保持されて使われます。RunSynchronouslyは説明のために使用しています。本当は使わないように書くべきです。Task.Waitに相当します。上記のような状態でSleepしているときにトークンがシグナル状態となると、正しくSleepが中断されます。Task.Delayの場合はキャンセルトークンを受け取るオーバーロードを使用していないと中断できませんが、F#非同期ワークフローの場合は、現在の非同期コンテキストに伝搬するトークンが管理され、Async.Sleepはそれを監視しているので正しく中断されるのです。プロパティで管理されていて、Async.Sleepはこれを参照しているからです。実際、Task.Delayと異なり、Async.Sleepにはトークンを明示的に指定するオーバーロードはありません。作ってみた感想この部分、私的にはとても良くできていると思いました。いうのも、F#非同期ワークフローの「非同期処理」に関する範囲と制約は、すべてワークフロー(computation式)内に閉じているからです。キャンセルトークンの管理もここで閉じているので、操作する側はAsyncクラスのインスタンス=非同期コンテキストと言うように、はっきりとイメージ出来ます。async-awaitではこれが曖昧で、ビギナーにasync-awaitの事を説明するためには、どこからが非同期コンテキストなのかをしつこいほどに解説する必要がありました。デフォルトではキャンセルに対する操作は完全に使用者任せとなり、Task.Delayのようにトークンを引き渡すシグネチャを明示的に設計に盛り込む必要があります。私はこのことに気が付くのが遅かったために、フレームワークインターフェイスの変更という大手術を行う羽目になったことがあります。周到に準備しても、使用者がトークンを渡すのを忘れると台無しとなり、トークンをどこで管理するのかということも(使用者が)考える必要があります。Taskとasync-awaitが後発でありながらこういう選択をした理由も恐らくあるのでしょう。想像出来ることと言えば、async-await方式は、メソッド内のコードに「await」という単語が入るだけで、それ以上の大幅な変化がないと言うことです。実用的な非同期コードをasync-awaitで書いたことがある方ならわかると思いますが、現実には非同期であることを意識したコードを書かないと、pitfallに落とされます(一例を例外の記事でも書きました)。F#非同期ワークフローでは、「async」に囲まれたcomputation式内でしか使えず、computation式では「let!」「do!」などの普通には使わない予約語を使う必要があります。上で述べたような問題とのトレードオフとしては悪くない選択だなと思います。説明も簡単なのが大きいですね。Asyncクラスはそれぞれ役割もインターフェイスも似ているにも関わらず、現実には設計思想からして全く違うものだという印象が強くなりました。後は、現在進行中のもう一つのプロジェクトと合わせて、何とかF#のコードが書けるようになったと言うことかな。人に説明するにはまだいろいろ足りてない感じですが、F#や関数型言語の面白さの一端は見えてきました。数回に分けて、継続についてのもろもろを、時系列にそって書きます。そもそも「継続」という学問があるようなのですが、私はここ最近に至るまでそんな学問があるとも知らずに継続を扱ってきたこともあって、きっと厳密性は全く無いと思います。うぅ、東京うらやましい...原点2000年よりも昔、まだインターネットが大学でしか使えなかったころ、巷ではシリアル通信によるパソコン通信なるものがコミュニケーションの主流でした。そのころに私は某BBSシステムを改良したシステムをフリーソフトウェア(この頃は定義さえ怪しい)として公開して配布していました。驚いたことに、これはいまだにベクターで公開されており、ソースコードも公開していたので、今回のネタの原点から振り返ってみることが可能でした(肝心のコードはあまりに古く、色々アレなので、ポインタについては勘弁してくださいw)。PASCALで書かれており、一部がアセンブラ(x86)です。プラットフォームはMS-DOSであり、RS-232C(シリアル)のドライバも一から書かなければならない頃です。そのBBSシステムは、多チャンネルに対応していたため、複数の電話回線から複数のモデムを通じてのセッションを同時に裁く必要があります。UNIXでいえば、物理的なシリアル何回線かに対して、シリアルコンソールでgettyに接続する感じでしょうか。現代的なコードであれば、スレッドを使って個々の接続を個別のスレッドで処理するような感じです。相手はMS-DOS、もちろんスレッドなんてありませんし、プロセスは分離されず子プロセスのみでコンカレント動作はありません。MS-DOSを知らない方には想像つかないかもしれませんが...となると、多チャンネルの操作は、ステートマシンの手動実装、つまり「ステート値」とその値に依存する「巨大なswitch-case」のような実装、そして細切れにされた各ステートの処理の乱立、という、酷すぎて保守したくないコードのようなものにならざるを得ません。このシステムには画期的な、ある小さなコードの断片がありました。endpcodeendsendこのコードはx86のアセンブラで、「transfer」と呼ばれるメソッドが定義されています。引数に指定されている「trans_stack」構造体へのポインタを使って、現在のスタックポインタを「無理やり別のスタック位置に入れ替える」操作を行います。このコードを見たときは「衝撃」でした(今でもよく覚えている。この方面のことに興味があるのは、これのせいかもしれない)。動作中にスタックポインタを書き換えるのです。このコードでは、proc2がどこからやってくるのかがよくわからないため、スタックポインタがどのような値になるのか想像もつきません。スタックポインタを書き換えた状態で、「pop」とか「ret」を実行しているのです。頭がパニックになります。先日のILのキホンでも言ったんですが、このような低レベルのコードは、「書いたとおりに動作する」のです。ILの場合は明らかに不正なコード片はCLRが止めてしまいますが、機械語の場合はほぼ書いたとおりに実行されます。スタックポインタがどこを指しているのかわかりませんが、その「どこか」から値をpopし、更に値を取得してそこに処理が遷移する(ret)のです。retは、その動作から、以下のようなジャンプ命令のように振る舞います。@tempというレジスタがあったと仮定して:pop@tempjmp@tempつまり、あらかじめ「新しいスタックポインタ」が示すメモリ領域に、bpに復元すべき値とジャンプ先のアドレスを書き込んでおき、そこを指すようにtrans_stack構造体を初期化しておいてtransferを呼び出せば、見事ジャンプ先に遷移することになります。こんなめんどくさくてわかりにくい事を何故行うのかというと、一度遷移に成功すれば、再びtransferを呼び出すことで、「以前使っていたスタックポインタの位置から処理を継続できる」からです。transferは「call命令」で呼び出されることを前提にしています。transferに遷移してきたとき、すでに戻るべき処理位置へのポインタはスタックに積まれており、これが次回のtransferからretするときに復元されます。PASCALで書かれていましたが、Cで書くと以下のような疑似的なコードとなります。}}よーく見てください。このコードを実行すると、mainのprintfとsubのprintfを交互に実行します。mainで10000回処理を行った後はプログラムが終了します。subは無限ループなので、終了しても放置されていますね。いずれにしても、このプログラムに「スレッド」はありません。環境はMS-DOSです。「transfer」を呼び出す必要があることを除けば、まるでスレッドが存在するかのようです。すれば、各「transfer-process」の暗黙のステートは、それぞれのスタック(mainのスタックと、mallocで確保されたメモリ)に隠されており、独立性を保ち、不要なステートマシンを作る必要がなくなります。各シリアル制御のコードは大幅に簡略化されるでしょう。実際、transferがなければまともなコードにはならなかったはずです。上記のsub関数内の自動変数「i」は、mallocで確保されたスタックメモリ領域内にその値が格納されています。コードのどこにも、明示的にmallocのメモリを読み書きしている個所がありませんが、これはスタックなのです。自動変数はスタック内に割り当てられます。subを実行するtransfer-processは、subの実行に必要な暗黙のステートを、スタック内で管理しているのです。transferを呼び出すたびに、このスタックは切り替えられます。このような構造を「コルーチン(coroutine)」と呼びます。この時にはこれが「継続」と呼ばれる何かだとは、全く気が付いていませんでした。Async.NET非同期処理(async-await)と例外の制御Taskクラスとasync-awaitを使用して非同期処理を記述する場合の、例外にまつわるあれこれをまとめました。非同期処理中に発生する例外を捕捉する方法は、一般的な例外の捕捉とほとんど変わりません。再スローthrow;}}}上記のコードにより:GetStreamAsync非同期メソッドの完了待機中(await)に発生した例外の捕捉それ以外の例外(例:SgmlReaderのパース)の捕捉の両方とも、catch構文で例外を捕捉出来ます。上記の例では示しませんが、C#5.0では、catchブロック中(又はfinally)で更に非同期メソッドを呼び出して待機(await)することは出来ません。C#6.0では可能です。Task.WhenAllを使って非同期処理を集約している場合、一度に複数の例外が発生するかもしれないため、発生した例外は「AggregateException」クラスに内包されてスローされます。上記のように、非同期メソッドを呼び出して例外を処理するコードを書いても、表面上は同期メソッド呼び出しと殆ど変りません。実際に考慮すべき事も殆ど変りません。内部で発生している例外がどのように処理されているのかという点では、かなり異なります。以下の図を見て下さい:(なお、まるでこれがHttpClientの実装であるかのように書いていますが、実際のHttpClientは別の手法で実現している可能性があります)これは、非同期メソッドではなく、同期メソッドでの例外のフローです(疑似的に、HttpClientに「GetStream」という同期メソッドがあると仮定して書いています。実際にはありません)。GetStreamを呼び出したものの、指定されたURLに接続出来なかったり、HTTPサーバーがエラーを返すなどした場合、メソッド内で例外がスローされます。それはそのまま呼び出し元のcatchブロックでキャッチされます。当たり前ですが、この時の呼び出し元スレッドの実行パスは、赤い矢印で図示した通りの経路をたどります。非同期メソッドを呼び出した場合はどうなるでしょうか?まず、GetStreamAsyncは非同期メソッドなので、異なる実行コンテキスト上で接続処理が行われます。「実行コンテキスト」とは、今はまだ「ワーカースレッド」と言うように読み替えて構いません。実行コンテキストが異なるという事は、呼び出し元スレッドの実行パスは、接続処理を行っているコンテキストとは別に、平行して処理が可能である事を意味します。非同期的に待機します。その間、まだ接続処理は別のコンテキスト(ワーカースレッド)で実行中です。ここで、接続に失敗するなどして例外が発生したとします。先ほど、実行コンテキストはワーカースレッドと読み替えて良いという話をしました。そのため、接続処理で発生した例外は、「そのスレッドコンテキスト内のcatchブロック」で捕捉されます。図ではそれを疑似的に示しましたが、これでは、ワーカースレッド上では例外を捕捉出来ても、呼び出し元では捕捉出来ない事になります。従来のThreadクラスによるワーカースレッド処理では、ワーカースレッドで発生した例外を、どうやって元のスレッドに通知するのかという事も、設計者が考慮する必要がありました。Taskベースの非同期処理では、例外を通知するのはTaskクラスの役割となっていて、一貫した操作が可能となっています。「.NET非同期処理(async-await)を制御する、様々な方法」でも書きましたが、能動的な方法で通知するのであれば、「TaskCompletionSourceクラス」を使えば、SetExceptionメソッドを使用して、呼び出し元のスレッドに通知する事が出来ます。余談ですが、仮に接続処理が「Task.Runメソッド」によって実行されていた場合、スローされた例外は自動的にRunメソッドによって捕捉され、呼び出し元のスレッドに通知されます。実行コンテキストとは非同期メソッド内の非同期的な処理を行う主体を、「ワーカースレッド」ではなく「実行コンテキスト」という抽象的な呼び方をしました。上記の説明の通り、非同期的な処理が実際にワーカースレッドで実現されているのか、又はそれ以外のなんらかの方法で実現されているのかは、メソッドの呼び出し元からは知る由もなく、知る必要のない、実装の詳細です(現実には把握したい場合もあるかもしれませんが)。APIでは一般的な「コールバック」です。Win32では伝統的に、処理の完了や失敗をコールバックで通知する事が多いのです。これは、Windowsのカーネルモード内部奥深く、デバイスドライバーが発する完了のタイミングの処理から、鮭が川を上ってくるように「逆方向」にメソッドが呼び出され、手元のコールバックハンドラメソッドが呼び出される、というように通知されます(「Async訪ねて3000里」を参照)。ここで、TaskCompletionSourceを使い、SetResultやSetExceptionを呼び出すことで、非同期処理の完了を通知します。従って、非同期処理とは、必ずしも特定のスレッドに紐づいた処理では無い事を意味します。そのため、「実行コンテキスト」という呼び方をしました。呼び出し元のスレッドにとっては、非同期メソッドである事の証は「Taskクラス」だけです。GetStreamAsyncメソッドの戻り値は「Task<Stream>」で、このインスタンスを「await」する事によって待機します。ここでも、スレッド基準ではなくタスク基準です。タスク基準の非同期処理の管理を行う場合に、タスクを認識して処理を行う主体に名前が欲しいと思う事があるため、特に「タスクコンテキスト」と呼んでいます(公式ではありません)。ここまでで、「非同期メソッド内の処理の主体」が、必ずしもスレッドに紐づかないという展開をしましたが、これがそっくりそのまま、呼び出し元にも当てはまります。呼び出し元は「await」した時点でスレッドによるハードブロックではなく、疑似的に非同期処理を待機しているかのように見せているからです。スレッドをブロックしない、という事は、この図のように、awaitの直前までのスレッドコンテキストは解放されることを意味します。そのスレッドがどこに行ってしまうのか?はともかくとして、ここでも実行コンテキストはスレッド基準ではなくタスク基準である事が分かります。GetStreamAsyncがSetExceptionで例外を通知する時、awaitしているタスクコンテキストは起こされ、catchブロックの処理を継続します。処理を継続するにはコードを実行しなければならないため、物理的なスレッドコンテキストが必要です。元のメインスレッドはawait時にどこかに行ってしまったかも知れませんね。新たに割り当てられたワーカースレッドが、代わりに処理を継続するかもしれません。Formsでは結局メインスレッドが再び処理を継続します。SynchronizationContextクラスの動作に踏み込むと本題と外れてしまうため、ここでは呼び出し元の処理もまた「非同期メソッド」となり、実行コンテキストはスレッド基準ではなくタスク基準となる、という事が分かれば良いと思います。タスクベース処理のデバッグ参考までに、タスク基準でのデバッグ手法について触れておきます。今まで、スレッドベースでのプログラミングに慣れてきたため、デバッグ時にはどのメソッドがどのメソッドを呼び出して来て今の状態に至るのかを「呼び出し履歴(スタックトレース)」を観察するのが基本でした。タスクベースの非同期処理をデバッグする場合は、スタックトレースを眺めても何も得られない可能性があります。タスクコンテキストはスレッドに紐づく場合もあればそうでない場合もあり、更にたまたま割り当てられたワーカースレッドによって実行コンテキストが得られている場合もあります。Studioのデバッガには「タスク」ウインドウが追加されました。このウインドウで、待機中のタスクコンテキストの状態と、待機が発生した位置を確認する事が出来ます。残念ながら、その時点の待機に至った履歴(疑似的な、非同期待機のスタックトレースのような)は見る事が出来ません。そのため、各タスク間にどのような依存性があるのかないのかは、このウインドウを見てもはっきりとは分かりません。将来的に改善されると良いなと思います。1.0がリリースされた時、言語仕様がJavaに似ている事から様々な点が比較されました。例外についてもかなり似通っているのですが、決定的に異なる点が少なくとも一つ存在します。それは、スタックトレースの扱いです。C#でやってはいけないと言われている事の一つに、例外の再スローの方法があります。ex;}}Javaからの移行組の人はついついやってしまいますが、C#ではこのコードはエラーになりませんがやってはいけません。ex」すると、ex内に記録されているスタックトレースの情報が失われ(リセット)、新たにスタックトレースが再構築されるからです。正しい再スローは以下の通りです。再スロー(スタックトレースは維持される)throw;}}throw句に何も指定しなければ、(例外処理ブロック中であれば)その例外が再スローされます。これは、IL命令でのrethrowに変換されます。特別な処理としてCLRに認識されているのです。この事は、前述の「Threadクラスで生成したワーカースレッド内で発生した例外を、元のスレッドにどうやって通知するか」と言う事に大きな制約を生じさせます。非同期処理の外側では、内部実装でワーカースレッドを使っているかどうかは関係なく、関心の無い事です。従って、ワーカースレッドを使っていたとしても、メソッド内で発生した例外は、あたかも連続したスレッドコンテキストで発生したかのように、スタックトレースが観察されて欲しい、と思う筈です。句以外では使えません。caughtException;}}このコードはコンパイル出来ません。再スロー構文「throw」は、catchブロック内でのみ使用出来るからです。caughtException」と書いてしまうと、スタックトレースを失ってしまいます。Remotingを知るまでは。CLR設計者に聞いたわけではありませんが、この仕様は恐らく意図的なものです。.NETは最初のバージョンから「アプリケーションドメイン」という考え方を導入しています。これは、「マイクロプロセス」と呼べるもので、同一のプロセス内に、サンドボックス的な分離構造を持たせる事が出来る機能です。同一プロセスであっても、異なるアプリケーションドメイン間の通信には大きな制約が生じます。これにより、アプリケーションドメイン間の安全性を高め、かつ、プロセス起動・終了による非常に大きなコストを排除するのが狙いです。Remotingは、このアプリケーションドメイン間の通信や、プロセスワイド、又はマシンワイド間での「リモート参照」機能を提供して、通信の実装負担を軽減します。リモート参照は一見してクラスやインターフェイスそのものに見えます。そこにはメソッドが定義されていて、メソッドを呼び出すとリモートのメソッドが呼び出されるという、非常に透過的で便利な機構です。補足:現代においては、CLRのリモート参照機能はややレガシーとして扱われています。APIが良く使われていますが、そこに皮を被せる形でリモート参照を使うケースはまだまだあるでしょう。アプリケーションドメインとは切っても切り離せない関係にあるため、当分の間、CLRリモート参照の機能が廃止されることはないと思われます。この「リモートメソッドの呼び出し」は、当然、引数や戻り値もハンドリングします。引数や戻り値をリモートメソッド間でやり取りするためには、インスタンスのシリアル化と逆シリアル化という非常に大きなトピックが含まれます。リモートメソッドで発生した例外は、そのまま呼び出し元に例外として通知されます。例外の通知を実現するためには、例外もまたシリアル化・逆シリアル化可能でなければなりません。シリアル化可能であるためには、例外クラスに含まれる情報が全てシリアル化可能である必要があります。そこには、例外のメッセージ文字列のような単純なものもありますが、「スタックトレース」も含まれているのです。スタックトレース情報は文字列ではありません(ToStringする事により文字列化されます)。スタックトレースは「StackTraceクラス」や「StackFrameクラス」を使って、実行時に動的にトラバースする事が出来ます。仮に、シリアル化が完全に機能するためには、これらのクラスの中身もシリアル化されなければなりません。StackFrameクラスは、リフレクションのMethodInfoクラスを保持しています。MethodInfoはこれが定義されているTypeクラスを参照し、Typeはそれが定義されたModuleやAssemblyクラスを参照しているでしょう。完全にシリアル化するためには、それらの型情報が保持された全てのDLLが特定されなければならず、逆シリアル化の際には呼び出し元でもすべての参照が解決されなければなりません。これは明らかに大げさすぎます。リモートメソッド呼び出し側は単に例外の種類(例外クラス)と、メッセージぐらいが判別できれば良いのかもしれません。例外がスローされることによって、リモート側のスタックの全貌が観察出来てしまうのは、セキュリティリスクです。このような理由かどうかは分かりませんが、CLRが再スロー可能なタイミングを強制しているのは:「いつでもスタックトレースが維持される」という状況は望ましくない場合がある。あるなら再スローは限られた状況下でのみ機能するようにデザインすれば良い。判断したように見えます。このようなCLRのデザインの為、残念ながらExceptionクラスは、スタックトレースを維持したまま、別のスレッドで再スローする事が出来ない事になります。.NETのスタンダードなソリューションとして、「TargetInvocationException」というクラスが、元の例外のインスタンスをInnerExceptionに保持してスローする事になっています。すれば、元の例外のスタックトレースを失うことなく、新たなスタックトレースを構築できます。元の例外クラスを指定してcatch出来ないため、「何このうっとおしい例外は!」と思うかもしれません。補足:TargetInvocationException.InnerExceptionに保持したからと言って、スタックトレースの物理的な情報をシリアル化するわけではありません。Remotingのインフラが、スタックトレースを文字列のような安全なデータに固定化します。そのため、リモート例外を受信したスレッドは、リモートのスタックトレースの「動的な」トラバースを行う事は出来ません。セキュリティ要件として、この機能を構成で無効化する事も出来ます。例外は捕捉可能でも、スタックトレースは全く参照出来ないようにする事も出来ます(むしろ、プロセス間以上のRemotingは、デフォルトが逆だったかもしれません。忘れてしまいました)。4.0・4.5以降)昔の話はこれぐらいにしておきましょう。4.0にてTaskクラスが導入され、タスクベースの非同期処理が可能になりました。これまで述べてきたスタックトレースの問題は依然として残っています。従って、awaitで待機中のタスクコンテキストに例外を通知する場合でも、迂闊にそのまま通知することは出来ない事になります。4.5からのサポートです。従って、上記のようなコードを書いても、await時にどうなるかは分かり難いですね。Task.Waitメソッドを呼び出してハードウェイトした場合、発生した例外は「AggregateExceptionクラス」という、新たなクラスに保持されてスローされます。使われ方はTargetInvocationExceptionと同じですが、AggregateExceptionは複数の例外を内包出来るところが異なります。つまるところ、それまでのスタックトレースは維持されるものの、結局スタックトレースは結合されず分断される、と言う訳です。Exception()」を発行した様子です。#0)」という行が見つかります。この行が、AggregateExceptionが内包しているExceptionクラスの例外のスタックトレースです。シームレスに繋がっているように見えるのは、ToStringがうまく文字列フォーマットしているだけで、本質的にスタックトレースが結合している訳ではありません。AggregateExceptionがありません。catch句はまるでExceptionクラスの例外を直接受信しているかのようです。実際に、catch句のexは、Exceptionクラスのインスタンスです。これでは、まるでスタックトレースがシームレスに結合しているようではありませんか!!重要な事です:awaitで非同期待機中に受信した例外は、スタックトレースが結合され、同期メソッドの例外と同じように振る舞います。今までは不可能な事だったので、これには仕掛けがあります。4.5で新たに追加された「ExceptionDispatcherInfoクラス」を使います。一般の開発者がこのクラスを直接操作するのは、あまり良い事ではありません。フレームワーク設計者はこのクラスを使って、スタックトレースを厳密に操作したくなるかもしれませんね。await待機中に受信した例外が全てAggregateExceptionにラップされてしまうと、async-awaitを使用したプログラミングはかなり非効率的になってしまいます。すべての例外はAggregateExceptionをキャッチし、改めて内包例外を判定する必要があります。これでは、細部を知らない開発者は「なんでこんな変な事になっているんだ」と文句を垂れる事になるでしょう。スタックトレースが結合され、AggregateExceptionを使わない事で、今まで掘り下げてきたことは何も知らなくても、とりあえずasync-await構文を使って普通にコードが書ける、という、最初の説明に繋がるわけです。CLRの世代を経て、結局スタックトレースが維持可能になったのは少し残念ですね。これではTargetInvocationExceptionやAggregateExceptionは何だったのかと思えてきます。́Д`)しかし、再度この問題をひっくり返すネタが、次の本題です。放置された非同期処理の行方さて、ようやく本題です。この記事を書こうと思った要因となるネタですが、今までの説明はこの話の導入に必要でした。以下のように、2つのI/O操作を効率よく実行するため、それぞれを非同期メソッド呼び出しで開始させ、その後awaitで完了を待ちます。ex){Console.WriteLine(ex.ToString());}}このような実装はデザイン的に避ける必要があります。一例を図示してみましょう。task2のバーで示しました。これらの非同期メソッドの内部で何が行われているのかは不明です。APIによって駆動されているのかもしれません。それらを緑枠で「タスクコンテキスト」として扱っています。二つのサブタスクはほぼ同時に開始されます。メインタスクとは関係なく非同期で動作するため、サブタスクのどちらが先に終了するのかは、良く分かりません。図中では、task1→task2の順で完了した場合を示しています。task1」として、task1の完了を先に待機しているため、通知もtask1→task2の順になります。非同期処理の順序がどのように処理されたとしても、両方の完了が正しく処理されるのです。...本当でしょうか?この例は、task1は処理が成功し、task2は失敗して例外をスローするパターンです。task2」にて例外が再スローされ、catchブロックでキャッチされます。何も問題なさそうですね?逆のパターンはどうでしょうか:task1の処理が失敗して例外がスローされた場合、メインタスクは例外を受け取ってcatchブロックで処理します。すると、非同期で動作中のtask2の結果(正常終了)を受け取る機会が失われます。task2」という文は実行されないからです。正常終了の通知を受け取らなかった場合、その通知はどうなってしまうのか?とても重要:どうにもなりませんこの通知は、メインタスクだけでなく、あらゆるタスクが受け取る機会を失います。従って、この結果通知は「無視」されます。このコード例では、WriteAsyncは結果を返しません(非ジェネリックなTaskを返します)。そのため、task2が成功しようが失敗しようが、結果を確認できなかったとしても、恐らく問題にはなりません。メインタスクの「関心」は、task1が例外をスローした時点で例外処理に遷移しているのです。さあ、問題の核心です:もう、何となくわかったかもしれません。この図では、task1もtask2も処理に失敗して例外を通知しようとします。task1はawaitしているので、その時点で例外が通知され、メインタスクはcatch句に遷移し、例外が処理されます。task2の例外は通知しようにも、受け取るタスクが無いのです。最初に解説した通り、async-awaitの構文は、殆ど同期メソッドを使用したコードと同じように、非同期メソッドの実装を可能にします。本質的には全く動作が異なるのです。この問題は、同期メソッドの実装の感覚で理解していると、ハマってかつ理解不能、というパターンかも知れません。task1とtask2を順番にawaitするというコードは、意図して書いたのであれば問題ありません。意図してという事は、「task2がどうなろうと知ったことではないが、task1が正常に終了するときには面倒を見たい」のような場合です。いう要求は、現実には殆ど無いと言って良いと思います。従って、このようなコードを書かないように注意する必要があります。この問題に対処する方法ですが、まず、成功はともかく、例外が完全に無視されてしまうのは問題と思われます。「無視されず、例外が発生すればいいんでね?」と思うかもしれません。その通りではあるのですが、受け取るタスクが存在しないのです。実は例外を発生させる方法はあります。それは、App.configに次のような指定を入れる事です。4.5移行で有効です。4ではasync-awaitが使えないので、これはあまり問題にならないかもしれません)。例外をスローしても、受け取るタスクが居ないんでしたよね。誰がどこからスローするんだ?という話になるのですが、実は「TaskExceptionHolder」という内部クラスが、「ファイナライザースレッドコンテキスト」でスローします。ファイナライザースレッドなので、これが分かったとしても何も出来ません。何かしても、もう手遅れで、この後プロセスは死亡します。そもそもこの例外を受け取れる可能性があるのは、「AppDomain.UnhandledExceptionイベント」しか無い、という事になります。そのようなわけで、もう少し使いやすいイベントハンドラがあります。「TaskScheduler.UnobservedTaskExceptionイベント」です。このイベントをフックすれば、発生した非同期例外と、その例外を「処理済み」としてマークするかどうかの選択肢が得られます。ログに記録してから無視する、等の方法が取れます。ex){Console.WriteLine(ex.ToString());}}しかし、これは面倒です。awaitの度に、例外が発生したか否かをいちいちcatchで確認しなければなりません。根本的には、Taskインスタンスを持ってまわるようなコードを書いた時点で、不測の事態が起きうることを覚悟する必要があります。してもこのようなコードを書かなければならないのか、は考える必要があります。(非同期例外であれば、AggregateExceptionに内包されている)Console.WriteLine(ex.ToString());}}原点回帰、というか、Task.WhenAllを使って複数のタスクを同時に待機します。今回の例はtask1とtask2が完了するのを待つ、というのが主目的なので、WhenAllを使えば目的は達成します。片方又は両方で例外が(どのような順序で)発生した場合でも、確実に例外を補足できます。ここで、AggregateExceptionの必要性が生まれます。前節でスタックトレースの結合が出来た事で、AggregateExceptionのような例外内包クラスは不要ではないかという例を示しましたが、非同期で発生する複数の例外を安全に呼び出し元のタスクに通知するために、この例外が必要になるのです。長くなりましたが、非同期例外にまつわる技術的な背景を書いてみました。安全な非同期処理が書けると良いですね。Async.NET非同期処理(async-await)を制御する、様々な方法async-awaitベースの非同期処理を制御する方法をまとめました。コードはわざと冗長に書いています。「できる!C#で非同期処理(Taskとasync-await)」Taskクラスの使用例として、ワーカースレッドを起動するという例が良く挙げられます。本記事も最初にTask.Runによるワーカースレッドの例を紹介していますが、本来のTaskクラスの役割はワーカースレッドの管理ではなく、非同期処理の統一された操作にあります(非同期I/Oもワーカースレッドも同じように「非同期処理」として管理可能にする)。async-awaitの使い方を調べていてこの記事にたどり着いた場合は、「基本的にTask.Runは使わない」と言う事を頭の片隅に置いておいて下さい。特殊な場合を除いて、明示的にTask.Runでワーカースレッドを操作する必要はありません。非同期メソッドが返すTaskクラスをawaitする事に集中すれば、問題なくコードを記述できる筈です。違いが分からなくなったり混乱した場合は、この前提を思い出してみて下さい。「.NET非同期処理(async-await)と例外の制御」Task.Runの基礎TaskクラスのRunメソッドを使用すると、ワーカースレッドを使用して独自の処理を非同期化(タスク化)出来ます。task;Console.WriteLine("Done!");}Task.Runメソッドは、.NET4.5未満ではTask.Factory.StartNewと同じです。Task.Runを使用した場合、戻り値としてTaskクラスのインスタンスが取得出来ます。従来は、Threadクラスを使用してワーカースレッドを直接生成して管理していましたが、Task.Runで返却されたTaskを使用すれば、非同期処理と同じようにワーカースレッドの操作を待機する事が出来ます。Threadクラスを使った場合、ワーカースレッドの待機は、Thread.Joinメソッドを使いますが、このメソッドはハードブロックしてしまいます。非同期処理の世界で行儀よく振る舞うためには、待機すべきあらゆる箇所でTaskクラスが必要となります。厳密にはGetAwaiterメソッドの実装が担保しますが、細部なので省略。WinRTでは、IAsyncActionやIAsyncOperationでも待機出来ます(後述)Taskを使用する優れた点は、他のタスクとの合成が簡単であることです。done!");}Task.WhenAllメソッドは、指定されたタスク群が全て完了するのを待機する、新しいTaskを返します。上記のように、CPU依存性の(つまり、ワーカースレッドで実行する)非同期処理と、ネイティブなI/O操作の非同期処理を、全く同じように扱う事が出来ます。Task.Runを使うと簡単にワーカースレッドを制御出来るので、従来のThreadクラスは、スレッド名を割り当てる必要があるCOMアパートメントを設定する必要があるスレッドローカルストレージに残骸が残るような状況を完全に破棄したいと言うような場合にだけ使用すれば良いでしょう。Task.Runの戻り値Task.Runは戻り値を返すことが出来ます。result);}Task.Run引数のデリゲートが戻り値を返す「Func<T>」である場合、返されるTaskも「Task<T>」となり、awaitすると戻り値が返されます。戻り値を返すタスクと、Task.WhenAllとLINQを使って応用すると、大量のデータを効率よくワーカースレッドに流し込んで並列実行させ、かつ全て完了するのを待機するという、今までなら出血しそうなコードが、非常に簡単に安全に記述出来ます。results));}慣れていないと分かり難いかも知れません。この処理は、以下のように動作します。この方法は、タスク(中身はワーカースレッド)を指定された個数分起動し、全てが完了するのを待ちます。直感的には、大量のワーカースレッドが生成され、コンテキストスイッチングで飽和してまともに動作しないように見えますが、実際はそうなりません。Task.Runは、スレッドプールからワーカースレッドを取得しますが、スレッドプールの総スレッド数は、効率よく実行できる程度に調整されています(図の例では、最大で4個のスレッドが次々と計算を処理します)。Explorerでスレッドに割り当てられているCPUサイクルを見たものです。このテストを実施したマシンは、4コア2論理スレッドなので、システム上は8スレッド使えます。実際にほぼ8スレッドだけがアクティブに動作し、大量のワーカースレッドで飽和する事が無い事が分かります。PLINQ(並列LINQ)と比べると、タスクを集約可能な式として実装する必要があるため、PLINQのように自然に拡張する事は出来ませんが、パフォーマンスの予測がしやすい事が利点です。Taskの存在しない世界に、Taskを導入するこのように、処理に紐づいたTaskがあれば、非常に応用範囲が広くなります。そもそも処理の完了をTaskで担保しない場合はどうでしょうか。CLRのイベントは一種のコールバックなので、対応するTaskが存在しません。具体的な例で考えます。WPFのボタンは、クリックされるとClickイベントが発火します。普通はこれをフックして、ハンドラとなるラムダブロックやメソッドで処理を実装します。この時、擬似的なTaskで発火状態を置き換える事が出来れば、様々な応用が可能になります。処理の完了を疑似的なTaskで表現するのに、「TaskCompletionSource<T>」クラスが使えます。(コードビハインドで書いていますが、推奨はしません。inputText;}SetResultメソッドを呼び出すと、待機しているタスクが戻り値を伴って継続します。上記の例は、ボタンのハンドラから直接テキストブロックに表示すれば良いので、何の為に複雑にするのかわからないかも知れません。イベントをタスク化する利点は、先ほど示したように合成が簡単だからです。(コードビハインドで書いていますが、推奨はしません。inputTexts);}上記の例では冗長に書きましたが、例えば動的に任意の個数で生成したテキストボックスやボタンの列に対して簡単に拡張・記述量の大幅削減が出来そうです。(コードビハインドで書いていますが、推奨はしません。inputTexts);}もう一つ例を示します。ワーカースレッドにThreadクラスを使わなければならない状況でも、TaskCompletionSource<T>を使ってタスク化する事が出来ます。tcs.Task;}このRunEx<T>は、Task.Run<T>のように使え、かつCOMのアパートメントを指定可能にする例です。TaskCompletionSource<T>は、例外を通知する事も出来ます。SetExceptionメソッドを呼び出すと、非同期待機しているタスクで指定された例外がスローされます。TaskCompletionSource<T>のジェネリック引数を指定しないバージョン(つまり、非同期待機後に戻り値を受け取らないバージョン)は存在しません。Task<T>は、Taskクラスを継承しているので、適当な引数型を与えて、完了もSetResultに適当な値を渡して代用する事が出来ます。非同期処理のキャンセル非同期処理をキャンセルするためのインフラも標準で用意されています。token);}}CancellationToken構造体は、関連する非同期処理全体にわたってキャンセル要求を通知するための構造体です。キャンセルが発生したかどうかを保持する、一種のフラグのようなものです。呼び出し側が何らかの事情でキャンセル要求を行うと、このトークン経由で通知されるので、非同期処理を中断させる事が出来ます。ThrowIfCancellationRequestedメソッドは、キャンセルが要求されているかどうかを検出し、キャンセルしていればその場でOperationCanceledExceptionをスローします。上の例ではトークンをWriteAsyncにも渡しているので、あまり大きな意味はありません(キャンセルされればWriteAsync内で例外がスローされる)。出来るだけ早くキャンセル要求に反応したい場合に、ThrowIfCancellationRequestedを使う事が出来ます。CalcellationToken自体は、キャンセル要求を通知するための抽象的な構造体に過ぎません。自分で呼び出し側のコードを書いている場合は、CancellationTokenSourceクラスを使う事で、制御可能なトークンを用意する事が出来ます。(コードビハインドで書いていますが、推奨はしません。canceled...";}}仮に、キャンセルさせたい場合の実行コンテキストが存在しない場合は、Registerメソッドを使って、コールバック化する事が出来ます。以下の例は、手元に実行コンテキストがある(awaitで待機している)ため、あまり適切な例ではありませんが、例えばネイティブAPIで処理開始後、処理がスレッドから完全に切り離されるような状況下で使用する事が出来ます。token);}}この他にも、WaitHandleプロパティから、待機可能なWin32カーネルオブジェクトが取得出来ます。APIと連携して同時にキャンセルを待機させたい場合などに使う事が出来ます。APIの呼び出しに対する非同期処理が「IAsyncAction」と「IAsyncOperation」インターフェイスで表現されています。これらは「IAsyncInfo」インターフェイスを継承していて、Taskクラスによく似ていますが、追加の情報(進行状況など)を通知する能力を持たせることが出来るようになっています。IAsyncActionとIAsyncOperationは、非同期処理の結果として戻り値を返すかどうかが異なります。Taskクラスの非ジェネリックバージョンとジェネリックバージョンに対応します。これらのインスタンスは「await」で待機可能です。IAsyncOperation<TResult>そのようなわけで、大体Taskクラスと同じように扱えますが、Task.WhenAllなどを使ったタスクの合成は出来ません。Task.WhenAllの引数は、Taskクラスしか受け付けないからです。そこで、IAsyncActionやIAsyncOperationをTaskクラスに変換する、ヘルパーメソッドが用意されています。results);}AsTask拡張メソッドを使用すると、Taskに変換できます。Taskに変換できれば、これまでの例と同じように応用が可能です。TaskをIAsyncActionやIAsyncOperationに逆変換する拡張メソッド(AsAsyncAction・AsAsyncOperation)も用意されています。WinRTの世界では、CancellationTokenに相当する、キャンセル管理用のクラスやインターフェイスがありません。その代わり、IAsyncInfo.Cancelメソッドがあり、直接キャンセルを要求する事が出来ます。TaskとCancellationTokenの世界をシームレスに結合する場合は、AsTaskのオーバーロードにCancellationTokenを渡すことによって、Cancelメソッドの操作も自動的に行わせる事が出来ます。2014」の21日目の記事です。Formsのような「コードビハインド」による実装の他に、「データバインディング」を積極的に使う実装を行う事が出来ます。いうよりも、データバインディングという手法を使わないと、複雑なUIを表現するのが困難です。この記事では、その辺りと非同期がどう関係するのかを書いてみたいと思います。Formsの時代、データバインディングと言うと「DataSet」クラスの内容を、「DataGridView」コントロールを使って表示させるという用途が一般的でした。詳しい解説は「Windowsフォームにおける「データ・バインディング」」を参考にして下さい。この手法の問題点として:DataSetやその周りのクラス(DataTableなど)と、List等の限定されたクラスがターゲット。実際には、IListインターフェイスやIBindingSourceインターフェイスを実装したクラスであれば受け入れる事が出来ますが、殆どの場合はこれらのインターフェイスを独自実装しないで、単にDataSetクラスを使用していたと思います。DataGridViewのセル内に、更に別の(コレクション)要素を配置するようなリッチな表現を、統一された方法で実現するのが困難。「DataGridViewのセル内に「コンボボックス」を配置するのはどうしたら良いのか?」と言うのは、定期的に発生するFAQであったと思いますが、このような拡張がスムーズに行えないのは、グリッドの表現をDataGridViewが独自に制御していたため、応用性が無く、学習効果が得られないからと思われます。バインディング対象のインスタンスがコンポーネントとしてフォームに設定される必要があり、柔軟性に欠ける。DataGridView以外のコントロールでも、バインディング機能を使う事は出来ました。バインディングしたいインスタンス(モデルクラスやコレクションなど)は、コンポーネントとしてフォームに配置される必要があったため、フォームとデータが密結合する事になり、データバインディングする事の意義が見出しにくい状態でした。コントロールへの直接アクセスの手法があまりに一般に広まったため、そもそもこの手法が利用可能である事も周知されませんでした。「DataSetを用意して、単純にDataGridViewに表形式で表示」させるのであれば十分なのですが、それ以上のUI表現に踏み込むと途端に難易度が高くなり、インターフェイスが複雑化してしまいます。DataSet自体がデータベースのテーブルを模倣したものであったため、階層化されたデータを容易に扱えないという問題もあります。Studioのデザイナー機能によって「型付きDataSet」を生成出来るようになりましたが、結局(生の)DataSetをラップしたものに過ぎず、本来コード上でモデル型として表現されている構造から乖離してしまう事は避けられませんでした。3.0でWPFが発表され、ここの部分に大きなメスが入りました。それが今のデータバインディングです。expression)」で実現しています。この手法には全く違いは無く、更にDataGridコントロールの場合は、階層化されたモデルクラスのインスタンス群をどのようにDataGridで表現するのかまでを、全てバインディング式で記述する事が出来ます。DataGridコントロールだけではなく、リスト形式で要素を表示する「ListBoxコントロール」や、コントロール要素の配置を制御する「Panelクラス」を継承したコントロール群「DockPanel・StackPanel・Grid等」も、ほぼ同様の手法でデータとの関連付けが可能です。これらのコントロールを階層化して組み合わせた複雑なUIは、データバインディングの規則に従っている限り、技術的な制限なく実装する事が出来ます。Formsのようにコントロールに直接アクセスして、ほぼ全てを制御する事も出来ますが、データバインディングを使えるようになると、かなり面倒に感じられるはずです。UIが複雑化するだけで実現難易度が高くなるため、単調なUIで実現する事しか選択の余地が無くなってしまいます。Formsでは更に難易度が高い)データバインディングのキモデータバインディングのキモは、コントロールの公開するプロパティと、モデルクラスの公開するプロパティの値を自動転送するところです。以下にTextBoxの例を示します。"ABC";}}TextBoxのTextプロパティは、テキストボックスに入力された文字列をやり取りするプロパティです。このプロパティに値を設定すれば、テキストボックスの初期表示文字列となり、ユーザーが文字列を入力したり編集したりすれば、それがこのプロパティから読み取れます。Formsと同様です。ここで、Textプロパティにバインディング式を記述します。バインディングするためには、相方のプロパティが必要です。MainWindowクラスに直接実装しましょう。/></Window>XAMLのコードについて、重要な部分が二カ所あります。一つはバインディング式を記述した事です。SampleText}」と記述します。この中括弧+Bindingで始まる指定がバインディング式です。「SampleText」と言うのは、バインディングさせたい対象のプロパティ名です。ここではMainWindowクラスに定義した「SampleText」プロパティを指定しています。一つの重要な点は、もはや「x:Name」による、コントロールへの命名が不要になっている点です。この点についてはまた後で説明します。MainWindowクラスへの変更も説明をしておきます。MainWindowクラスにも重要な二カ所の変更点があります。一つはバインディング対象のプロパティ「SampleText」を追加した事です。これに合わせて、値を保存しておくためのフィールドも追加してあります(熱心なC#信者なら、自動実装プロパティを使う事が出来ると思うかもしれません。デバッガで動きを追えるようにするためにわざと手動で実装しています)。一つはコンストラクタに追加した、「DataContext」プロパティにthisを代入するという式です。このプロパティは、XAML上でバインディング式を実行する際に、デフォルトのバインディング対象のインスタンスを特定するために使われます。ここでthisを代入しているという事は、デフォルトのバインディング対象はMainWindowクラスのインスタンスとなる、と言う事です。SampleText}」となっているので、結局「MainWidnowクラスのインスタンス」の「SampleTextプロパティ」が、TextBoxのTextプロパティにバインディングされる、と言う事になります。このコードを実行すると、望みの通り、バインディングが実現し、初期状態としてフィールドの値が表示されます。テキストボックスの文字列をタイプして変更しても、SampleTextプロパティのsetterに遷移しません(ブレークポイントを設定して確かめて下さい)。これは、バインディング粒度のデフォルトが、コントロールからフォーカスが外れた時、となっているためで、サンプルではTextBoxが一つしかないためにそのタイミングが無い事によります。ここでは単純に、「一文字でも変化があれば」、と変えておきます。この指定もバインディング式だけで実現出来ます。/></Window>XAMLのバインディング式に「UpdateSourceTrigger=PropertyChanged」と付け加えます。すると、バインディング処理の実行が、Textプロパティに変化があった時という条件となります。これで、setterにブレークポイントを張っておいて、テキストボックスで「A」をタイプするとこのようになり、バインディング式によって自動的に値が転送されて来る事が分かります。上記のデータバインディング例を図示すると、このようになります。Formsのように、イベントハンドラのフックやTextBoxのTextプロパティへの直接アクセスをする事無く、値の受け渡しが出来ている事になります。おっと、忘れる所でした。これで「x:Name」によるコントロールの命名が不要になっている理由も分かりますね。プロパティ同士がバインディング式で直接転送されるので、「どのコントロールに対する操作なのか」の明示は不要となっているのです。Formsっぽい手法からスタートして説明するため、バインディング対象のインスタンスをDataContextプロパティにthisを代入する事で「MainWindow」そのものにしましたが、更にコードの責務を分離するために、バインディングするクラスを独自のクラスに完全に分離することも出来ます。「MainWindowViewModel」クラスを作り、DataContextにこのクラスのインスタンスを代入する事で、バインディング対象をMainWindowから切り離す事が出来ます。MainWindowViewModel();}}これが「MVVM(Model-View-ViewModel)」パターンの入り口です。APIのセットは、原則として外部リソースアクセスは全て非同期で実現しなければならないようになっています。ブログの記事や勉強会でも説明した通り、これらはC#で言う所の「async/await」を使って実現します(より深くはTaskクラスやIAsyncInfoインターフェイスを使う事ですが、本題と関係ないので省略)。「非同期処理」と「ユーザーインターフェイス」とはどう絡むのかが本題です。この問題は古典的でもあって、一般的なウェブブラウザのフォームでも同じような問題を抱えていました。「購入」ボタンを何度もクリックされると困るので、クリック後はボタンを無効化して押せなくする等の処置は、(ブラウザとは無関係に)ウェブサーバー側で購入処理が非同期で実行されるから発生する問題でした。何度「購入」ボタンが押されても問題ないように、同じセッションが維持されていると分かる場合は、単一の要求と見なすなどです。どちらの手法を取るにしても、「非同期処理は何も対策しないで実現することは無理」と言う事です。async/awaitのサポートによって、非同期処理の実装難易度は格段に改善されたのですが、この部分をどうするのかは、仕様や実装を行う者に任されています。二つのパターンの背景を説明しましたが、非同期処理をユーザーインターフェイスを工夫する事によって担保する方法について、データバインディングと絡めて考えてみます。非同期処理中にユーザーインターフェイスをロックダウンする、と言うのは、もっとも単純で効果的な方法です。真っ先に思いつくのは、Windowsクラス全体のIsEnabledをfalseに設定する事でしょう。これをいつ行えばよいでしょうか?というよりも、非同期処理をどこで開始するのか?と言う事です。UIにボタンが配置されており、ボタンのクリックと同時に非同期処理が開始されるとします。データバインディングを使うのであれば、「ICommandインターフェイス」を実装したクラスを使って、ボタンクリックのイベントを「バインディング」するでしょう。移譲されたイベントハンドラは、以下のような実装となります。(これは擬似コードのため動きません。true;}}}この例では、ボタンのクリックと言う「アクション」を引き金に動作するので、比較的分かりやすいと言えます。void」と定義するのはイベントハンドラの場合だけだと述べられています。この例は本物(CLR)のイベントハンドラではありませんが、ICommandによって間接的に呼び出されるコールバックとして、広義において同じと見なしても良いと思います。値を転送するデータバインディングそのものが引き金になる場合はどうでしょうか?前フリのような、テキストボックスに対する編集操作で、非同期的に処理を実行し、結果を反映する場合です。Studioのインテリセンスがシンボル名の絞り込みを行う過程や、ウェブで言うなら「googleサジェスト」のような機能です。この場合、バインディングされたプロパティに、一文字変化がある度に値が転送されてきます。転送はsetterのコールによって実行されるため、非同期処理はsetterから始める必要があります。set;}}このコードには仕様的にも実装的にも問題があります。仕様面では、一文字入力する度にWindowが無効化される(しかも非同期処理が完了するまで解除されない)ため、恐らくユーザー体験は最悪の物となる事です。実装的な問題としては、setter内ではawait出来ない事です。これはsetterにはasyncを指定出来ない事から来ています。実装的な問題は、実処理をメソッド抽出すれば解決します。非同期メソッドを呼び出すが、待機は出来ない。voidとする事も出来ますが、ここではより汎用的になるように(そしてベストプラクティスに従うように)、Taskクラスを返却するようにしました。setterで戻り値のTaskを無視すると、C#コンパイラが警告を発します。そのため、ダミーの変数で受けるように書いています。Taskクラスを返す事で、別の仕様要件でProceedTextAsyncを呼び出さなければならない場合にも、非同期処理の待機が可能になる余地を残す事が出来ます。残る仕様的な問題ですが、ウインドウ全体を無効化する必要が無いのであれば、特定のコントロールに絞って無効化を実施するという方法が考えられます。検索結果に応じてリストボックスの内容を変化させる場合、テキストボックスは有効としたまま、リストボックスだけを無効化するようにすれば良いのです。その場合、ロジックの変更は必要ありません。XAMLのバインディング式をListBoxに適用します。--></DataTemplate></ListBox.ItemTemplate></ListBox></DockPanel></Window>非同期処理のエラーの対処ここまでで、データバインディングで値が転送された場合の非同期処理は対処可能になりました。非同期処理中に発生するエラーはどのように対処すれば良いでしょうか?そもそも、仕様的な面で非同期処理中のエラーにどう対処するかを考える必要があります。最後の例では、ユーザーがテキストボックスに何かタイプすると、非同期で検索処理を行い、結果がリストボックスに(非同期的に)表示されます。一文字タイプし、その事が原因で何らかのエラーが発生したとしても、ユーザーはもう次の文字をタイプしているかも知れませんし、ウインドウ上の別の機能を実行しているかも知れませんし、ウインドウを閉じているかも知れません。エラーが発生したら、その場でメッセージボックスを表示する。単純なユーザーインターフェイスであれば、これでも良いと思われます。複数の非同期処理を実行している可能性があり、それらが一斉に失敗すると、メッセージボックスが大量にスクリーンに表示される可能性があります。忘れた頃にいきなり表示される可能性もあります。ログ出力のような表示領域に表示させる。過去に発生した内容を表示できるので、より望ましいと言えます。どのメッセージが何の操作に紐づいていたのかは分かりにくくなるかも知れません。どのような方法でも、握りつぶすのでなければ、対処方法は同じでしょう。非同期処理の実質的なコードが例外をスローする場合は、単にtry-catchで例外を補足し、エラー処理を行います。5.0ではcatch-finallyブロック内でawait出来ない事です。WPFの場合はMessageBoxの内部でメッセージポンプが実行される(Win32メッセージループが実行される)ので良いのですが、ストアアプリではawaitで待機する必要があります。そのため、多少泥臭いコードを書く必要があります(エラーを保存して一旦catchブロックから抜けてから表示するなど)。エラー処理のコード例をお見せしましたが、結局のところ、一般的なコードと何も変わらないことが分かると思います。仕様がより高度になるにつれ、データバインディングを実装しているクラスを分割したくなります。ユーザーインターフェイスに対応するための固有のコードと、いわゆるビジネスロジックの分離です。この時、前述の例で示したHttpClientによる検索処理などのビジネスロジックは、ビューモデル(元のクラス)からモデルに移動する事になります。実装を移動する際、HttpClient.GetStreamAsyncは非同期メソッドなので、await可能でなければなりません。言う事は、移動先のメソッドにはasyncが適用されなければならず、ベストプラクティスに従うならTaskクラスを返却するメソッドでなければなりません。結局のところ、「ProceedTextAsync」メソッドはほぼそのままの形でモデルクラスに移動する事になります。ProceedTextAsyncがビューモデルに依存している箇所は、それぞれを以下のように対処します。モデルはthis.ResultItemsにはアクセス出来ない。したがって、ProceedTextAsyncは結果をまとめて(コレクションに入れるなどして)返却する。モデルは直接UIとの関係を持たないと考えると、IsEnabledの操作はビューモデルに残しておく。モデルは直接UIとの関係を持たないと考えると、メッセージボックスの操作(例外の処理)はビューモデルに残しておく。以下の例では、モデルクラス自身をコレクション(List)とし、UpdateAsyncメソッドでここに結果を保持するようにします。モデルの存在意義を考えてこのようにしましたが、実際の所はメソッドを単なる移譲として分離(戻り値にコレクションを返すスタティックなメソッド)した方が良かったかも知れません。});}}}このように分離すれば、あとはビューモデルから呼び出すだけです。true;}}更なる発展コレクションは段階的に反映したいですね。結果が認識されるたびに、1件ずつリストボックスに反映されたりすると、ユーザーインターフェイス的にとても良いと思います。このような実装にするには、結果が1件取得される度に、バインディングしているコレクションに反映して、それがUIにも反映される必要があります。コレクションへの反映はINotifyCollectionChangedインターフェイスで通知することが出来ます。このインターフェイスはObservableCollectionクラスが実装しているので、Listの代わりにこのクラスを使うと良いでしょう。注意点として、コレクションへの追加処理は、UIスレッド(WPFでは恐らくメインスレッド・ストアアプリではビュー毎に割り当てられているスレッド)で実行する必要があるという事です。ここにトレードオフがあります。CPU依存性処理をワーカースレッドで効率よく処理させるために、「ConfigureAwait」メソッドを使用したくなるかもしれません。これにより、awaitの度にUIスレッドにマーシャリングされなくなるため、パフォーマンスが向上します。当然の事ながらUIスレッドにマーシャリング出来ないので、(メソッドを抜けるまでは)UIに絡む処理は実行出来ません。ObservableCollectionを使用した場合、要素の変更時に「CollectionChanged」イベントを発火します。このイベントの発火はUIスレッドで実行しなければならないため、ObservableCollectionの操作は暗黙にUIスレッドで実行する必要があります。前述のコード例でConfigureAwaitを使用しているのは、結果を一旦Listに保存しているので、直接的にも間接的にもUIに依存していない事が分かっているためです。このように、呼び出し元のスレッドがどこまで有効に作用するのかを考えて、ロジックを分離すると良いでしょう。この辺りを掘り下げた資料は、以前の勉強会で発表した資料があるので、参考にどうぞ。今年もあと少し、今日は寒くて堪りませんでした。明日はmoririringさんです。Studioで普通に作ると、以下のようなテンプレコードが生成されます。0;}}}勉強会のasync/awaitシリーズで説明したシチュエーションは、WPFやWinFormsという環境での話でした。そこでは、「UIキュー」が重要で、このキューに非同期処理の完了が放り込まれ、メインスレッドがこれを拾い上げて処理する事で、await以降の処理が実行されることを説明しました。コンソールで実行されるコードの場合、この「UIキュー」が存在しません(WPFやWinFormsのインフラを使った場合は生成されますが、ここでは扱いません)。そのため、await時に「ハードウェイト」しても困らない事になります。UIキューが存在する環境でハードウェイトすると、UIキュー内のエントリが実行されなくなります。目に見える問題として、ユーザーの操作(ウインドウを移動したりサイズ変更したり、ユーザーインターフェイス要素の操作)が無視され、アプリケーションが固まっているように見えます。コンソールアプリケーション実行時のウインドウ「コマンドプロンプト」は、アプリケーションのスレッドの状態に関わらず、常に操作可能です。以下のProcessExplorerのスクリーンショットは、コマンドプロンプトウインドウを激しく動かしている時に撮ったものです。コンソールアプリケーション(ここではcmd.exe)を起動すると、同時に子プロセスとして「conhost.exe」が起動します。したことは分かりませんが、上記のようにウインドウの操作を行うと、このconhost.exeの負荷が上昇する事から、ウインドウメッセージの処理はこのプロセスが受け持っている事が推測できます。あれば、実際のコンソールアプリケーションは、UIキューの処理に影響される事無く(元々持っていない)、そしてユーザーインターフェイスがブロックされる事が無く、動作するのです。話を戻して、コンソールアプリケーションではUIキューを持っていないので、スレッドがハードブロックしても問題ありません。Taskクラスを返却する「非同期メソッド」は、必要な個所でハードブロック出来ます。この事を利用して、以下のようなスケルトンコードを考える事が出来ます。Marshal.GetHRForException(ex);}}}}Mainの最初の処理として、InternalMainAsyncメソッドを呼び出します。このメソッドはTaskクラスを返却する非同期メソッドです。そのため、このまま放置すると次の処理に行ってしまいます(Mainを抜けて終了してしまう)。そのため、Waitを呼び出してハードブロックする必要があります。上記の例ではResultプロパティを呼ぶことで、間接的にハードブロックしています(戻り値intを取得するため)。InternalMainAsyncの中の処理は全て非同期で記述できます。ここでの知識は、WPFやWinFormsで非同期処理を記述する時と全く同じ知識を活用できます。言い換えると、普段から非同期処理を書いているなら、同じ流れで書いて問題ないという事です。メインスレッドとUIキューの関係は無く、メインスレッドはResultでハードブロックしているので、全ての非同期処理(非同期処理の継続操作)はワーカースレッドのコンテキストで駆動されます。マルチスレッドアクセスが認められないクラスやメソッド、特定のスレッドコンテキストに紐づくクラス等を扱う場合は、スレッド同期やマーシャリング等の操作が必要になりますが、この辺の知識はTask.RunやThreadクラスを使って手動でワーカースレッドを作った時と同じように考えればOKです。第30回@名古屋ソフトウェアセンタープログラミング生放送勉強会で「プロ生ちゃんをひろっちゃう!」というタイトルでセッションに登壇してきました。いうことなのか、良く分からない...仕方が無いので、プロ生ちゃんベースに、いつもとは違う方向性でセッションしました。精進出来るようにがんばります。内容はざっくり、プロ生ちゃんサイトの壁紙ページをスクレイピングして、非同期で画像をダウンロードしながらWPFでデータバインディングして表示するという内容です。スクレイピングするために、どうやってサイトのHTMLを調査するか、そして実際にスクレイピングする時に使えるライブラリや、HTMLの解析方法、ユーザーインターフェイスをスムーズにする為の非同期処理の概要をちりばめました。以下にスライドを置いておきます。例によってソースコードはGitHubに上げてあるので、見る事が出来なかった方は参考にして下さい。Fifteen.%d人のブロガーが「いいね」をつけました。