土屋つかさの技術ブログは今か無しか土屋つかさの技術ブログは今か無しか読者になる土屋つかさの技術ブログは今か無しか土屋つかさが主にプログラミングについて語るブログです。実装するのに専用構文が必要なのか?」「async/awaitは具体的にどのような挙動をしているのか?」などにしっくり来ていない方におすすめです。説明を優先するために非同期処理において重要な概念のいくつかの説明を省略したり、簡略化したりしています。最後に必要と思われる最低限の補足を書いているので、そちらも参考にしてください。参考リンクに載せたサイトで、より詳細な解説を得られます。想定する状況ソシャゲの起動時に表示する「お知らせ」の実装を考えてみます。あの手の表示は、ネットごしにhtmlを取得し、それをwebエンジンに丸投げして表示させるのが一般的かと思います(それが一番楽な実装だから)。この「ネット越しにhtmlを取得する」という処理は、非常に時間がかかります。後ほど実際にやってみますが、場合によっては200ミリ秒(約12フレーム)以上かかる場合もあるでしょう。これをそのまま実装すると、12フレームの間ユーザーからの入力を受け付けないことになってしまいます。これをなんとかしなければならないというのが今回のミッションです。最初に、今回使うサンプルコードのガワは以下になります。コンソールアプリケーションで、RenderInfomation()メソッドを呼び出すとhtmlの取得が行われます。メソッド呼び出し前後で文字列を出力し、また、メソッドを呼び出してから戻ってくるまでをストップウォッチで計測しています。}}次に、RenderInfomation()メソッドから呼び出す2つのメソッドを作っておきます。「ネット越しにhtmlを取得するメソッド」と、「取得したhtmlを描画するメソッド」です。htmlを取得するメソッドはこんな感じ。指定したurlからhtmlをまるごと取得して返します。}受け取ったhtmlを描画するメソッドを実装するのは大変なので、取得したバイト数と中身をコンソールに出力するようにしておきます。}以後、この2つのメソッドを実行する方法を考えていきます。パターンA:非同期を使わないまず、非同期では無い場合からはじめましょう。先程の2つのメソッドを単に実行します。}実行するとコンソールに以下のように表示されました(注意:数値はあくまで目安です。スレッドの初期化コストやキャッシュの有無など、様々な要件で値は大きく増減します)Test1メソッドを呼び出してから抜けるまでに685ミリ秒(60fps換算で約42.0フレーム)かかっています。その間ユーザーからはフリーズして見えるわけです。これをどうにかしていきます。パターンB:Task.Run()を使ってみる(非同期処理を使っているが結果的には同期)早速非同期処理をやってみましょう。先にダメな例から見てみます。以下はhtmlの取得を別のスレッドで実行するコードです。}1Task.Run()は引数で渡したタスク(非同期で処理するコードの単位のこと。ここではラムダ式)を別スレッドで実行し*1、そのタスクのハンドル*2であるTask<TResult>オブジェクトを返します。ここではTask<string>を宣言し、GetHTML()の戻り値でstring型を受け取れるようにしています。タスクは別スレッドで実行されるので、メインスレッドはすぐ2の処理に進みます。2Task.Resultプロパティは、評価されるとタスクの処理が完了するまで待機し、完了したらそのタスクの戻り値を返します。ここではGetHTML()の戻り値がresultに格納されます。htmlの読み込みが終わってから処理がMain()に戻っています。「完了するまで待機」しているので、結果的に同期処理と同じ結果になっているわけです。これでは非同期処理とは言えません。パターンC:ContinueWithを使った非同期ここからが一般的な非同期処理になっていきます。}1Task.Run()で別スレッドを実行しTaskオブジェクトを受け取る処理はパターンBと同じです。タスクは別スレッドで実行されるので、メインスレッドの処理はすぐ2に続きます。2Task.ContinueWith()は「『タスクが終了したら実行するメソッド』を登録する」というメソッドです(ここではラムダ式を設定しています)。登録しているだけなので、RenderInfomation()は終了してMain()に戻ります。別スレッドの方では、GetHTML()が終わった後でtask.Resultからhtmlデータを受け取りEvalHTML()を実行します。先程と異なり、"37079バイト読み込みました"が一番最後に来ています。これは、RenderInfomation()がhtmlの読み込みを待たずに処理をMain()に返し、Console.ReadLine()で待っている間に別スレッドの処理が終了してEvalHTML()が値を返した事を示しています。バリエーションちなみに、上のコードは以下のように書く事もできます(同じ処理になります)。}【ここが重要!】もうちょっと読みやすくできないものかパターンCの実装は正しく非同期処理を実現しています。難しく言えば、可読性やメンテナンス性に優れているでしょうか?今回のサンプルは簡単で短いので読みにくいということは無いかもしれません。非同期処理の実装は容易にコードを複雑にしてしまえることが知られています。GetHTML()で受け取った値から新規にurlを生成し、そのurlを使って更にGetHTML()で別のサイトを読み込むコードを考えてみます。新規にurlを生成するのは大変なのでCreateNewURL()というモックメソッドを使います。}2個目のGetHTML()がurlを受け取るには、1個目のGetHTML()の終了を待たなければなりません。そこで、Task.Run()をネストし、ContinueWith()を2回実行します。このように、非同期処理を連続で実行するにはネストが必要で、コードの可読性/メンテナンス性が落ちます。非同期処理を連結したり、前のタスクの結果によって次に実行する非同期処理を分岐で切り替えるようにしたりすれば、より複雑なコードになってしまうことが想像出来るかと思います。ちなみに実行結果は以下のようにちゃんと非同期になっています(「受け取った値をベースに~」を出力する処理はサンプルコードにはありません)。典型的な非同期処理では、ここまで説明してきたような「非同期で実行したタスクの完了後に、タスクの結果に演算を加える」というコードが頻出します。多くのプログラミング言語ではこれを簡潔かつ汎用的に記述することが難しく、これまで様々な試行錯誤が行われてきました。そんな中、C#*3では、この問題を言語機能レベルで解決することにしました。そのためにC#ver5.0から導入されたのが、await/async構文なのです(やっと出てきたよasync/await!)。パターンD:async/awaitを使った非同期(非同期メソッド)それでは、async/awaitを使って実装してみます(以下は同じ結果になるので出力画面は省略します)。modifier)で修飾します。ちなみに、このasyncは文法仕様上は本来無くても良いのですが、async/await構文導入前のコードでawaitを識別子に使用しているコードで問題が起きないようにしてあるそうです*4。operator)を記述します。この記述によって、コンパイラは、この式より後にあるコードを「タスクの完了後に実行されるコードである」と見なし、パターンCのようにContinueWith()を使った場合とほぼ同じコード*5を生成します。パターンCと比べると、可読性の高いコードになっていることが分かると思います。バリエーション【ここが重要!】で書いた、非同期処理を連続して実行するコードも書き換えてみましょう。}ネストがなくなり、(非同期では無い)通常の処理のように記述できます。(補足)パターンE:RenderInfomation自体を非同期メソッド化するここまでで入門編は終わりで、最後に補足を。実はこれまで説明したパターンA~Dは非同期処理として不完全でした。いうのも、WebRequest.GetResponse()は同期処理でして、実行されるとWebレスポンスを取得し終わるまでそこで待機してしまうのです。Webレスポンスを非同期で取得するにはWebRequest.GetResponseAsync()を使います。これを使うことでGetHTML()メソッド自体を非同期メソッドにすることが出来ます。修正したGetHTMLAsync()メソッドと、それを呼び出すRenderInfomation()を以下に示します。}1新規に実装したGetHTMLAsync()をawait演算子を付与して実行します。2GetHTMLAsync()の宣言をasyncで修飾し、非同期メソッドとします。3webRequest.GetResponse()の代わりにwebRequest.GetResponseAsync()メソッドをawait演算子を付与して実行します。非常に少ない修正で、GetHTML()を非同期メソッド化することができました。GetHTMLAsync()の戻り値はTask<string>ですが、RenderInfomation()内ではstringを返すように記述できている点に強い違和感を覚えますが、これこそ、非同期処理の複雑な部分をコンパイラが裏でコードを生成して解決している証左とも言えるでしょう。終わりに以上で説明を終わります。説明を省略している箇所も多数あるし、正確で無い説明になっている箇所もあるかもしれませんがご容赦ください(コメントでご意見頂けると嬉しいです)。Unityでasync/awaitを使うのに最低限必要な解説にはなっていると期待したいです(とはいえ、土屋自身が非同期コード書いたことがほぼ無いのでわからん)。Unityでasync/awaitを何に使うのかという点については、基本的にはI/O処理になると思います。ステートマシン管理に使うのはオーバースペックでしょう(使えるのかも知らない)。本論から外れますが、土屋自身はyieldコルーチンを使った状態管理も懐疑的です(プッシュダウンステートマシンの記事を参照)。補足:この記事で説明していない概念について今回説明を省略した物のうち、いくつかについて簡単に書いておきます。context)複数のスレッドがGUI、つまり描画処理を行おうとした場合、GPUに描画命令を出している最中にスレッド切り替えが起きるとまともに動作しないため、あるスレッドが描画中はGPUを占有する必要があります。このようにマルチスレッド時に処理が確実に動作するように実装することをスレッドセーフと言います*6。スレッドセーフな実装は非常にコストが高く、GUI描画のように頻出しかつ速度が求められる処理では避けたい所です。そこで、多くのプラットフォームでは、GUIフレームワークは予め決められた1つのスレッド(UIスレッドと言います)のみで動作するように設計されています。これはDirectX/OpenGLなどの低レイヤライブラリでもそうなっているので、Unityを使う場合にも同様です。一般に、UIスレッドはメイン処理が動いているスレッドです。そのため、今回のように非同期でhtmlを取得してからそれを描画したい場合、描画する前に元のスレッドに切り替える必要があります。context)」と言います。同期コンテキストは以下のように設定します。TaskScheduler.FromCurrentSynchronizationContext());このように、ContinueWithの第2引数にUIスレッドの同期コンテキストを渡しておく(これを「同期コンテキストを拾う」と表現するようです)と、task完了後の処理はUIスレッドで実行されるようになります(ちなみに、コンソール出力はUIスレッドでなくても実行できます)。await/asyncでは、標準で同期コンテキストを拾い、次の行から元スレッドで動作しています(敢えて切り替えないようにするにはオプション設定が必要です)。......と、ここまでが教科書的な説明なのですが、Unity用高性能非同期ライブラリUniTaskではUIスレッドに戻す処理を省略しています。Unityではこの処理をコアコード(C++)側で行うため、C#で対応する必要がないようです(未確認)。スレッドプール記事中では何度か「タスクを別スレッドを実行し」と書いていますが、これは「タスクをスレッドプールのキューにスタックし」が正しいです。この「スレッドプール」について説明しておきます。必要に応じて新規スレッドを生成/削除するというのは非常にコストの高い処理で、頻繁に実行するとシステム全体の処理能力が低下してしまいます。そこで、一般的な処理系では、予め複数のスレッドを立てておき(これを「スレッドプール」と言います)、空いているスレッドにタスクを振り分けています。Task.Run()が実行されると、タスクは一度タスクキューにスタックされ、システム側でスレッドプールの中から空いているスレッドが選択され(空きがない場合には新規に作成し)、そこにタスクが割り当てられるのです。コンパイラがasync/awaitをどのように展開するか記事中では「コンパイラはContinueWith()を使った場合とほぼ同じコードを生成する」と書いていますが、実際には他にもいくつかの処理が生成されています。そのうちの1つは、先程説明した同期コンテキストで元スレッドに戻す処理です。他にも、非同期処理が一瞬で終わった場合にはそもそも待機しないように分岐させたり、awaitを連続で記述した場合にも正しく動くようなステート管理の処理が生成されています。Engineers'Blog)tech.cygames.co.jpマイクロソフトによるUnityでの.NET4.x対応についての記事。リンク先はAsync/Awaitの話docs.microsoft.comUnitySynchronizationContextについての解説。docs.unity3d.com*1:正確にはスレッドプール上のキューに配置する。スレッドプールについては後述*2:ある処理に間接的にアクセスする為のエントリポイントのこと。昔は生のアドレスを関数ポインタとして使っていた。Injection)コンテナに...「DIコンテナ」という言葉を聞いたことがあっても、それがな...2020-01-10プッシュダウンステートマシンのサンプル実装(階層構造を扱える状態遷移機械の話)プッシュダウンオートマトン形式のステートマシン(以下プッシ...2017-12-05DXTnm形式とはなにか。10サイト情報プリバシーポリシーはてなブログをはじめよう!t_tutiyaさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。