18.07/12Unityでasync/await使えてハッピー。大々的に使われだしてはいないようです。理由の一つとして、Unityが標準でサポートする気が全くなさそう。少なくとも、Unityがフレームワークとしてasync/awaitには何一つ対応していない。async/awaitという道具立てだけじゃあ何もできないのです、フレームワークとして何らかのサポートがなければ機能しないわけですが、なんと、何もない......。何もないことの理由はわからないでもないです。System使ってくれというのは理にかなっている(私もそちらが良いと思います、つまりTaskのマルチスレッドな機能は原則使わない)。いえ、async/awaitは便利なので、このまま、便利だけど性能は微妙だから控えようみたいな扱い(あ、それ知ってる、LINQだ)になるのは嫌なのよね。LINQは局所的なので使わないのは簡単なのだけど(実際、最近は私もあまりLINQ書いてないぞ!遅いからね!)、async/awaitは割と上位に伝搬していって汚染気味になるので、そもそも一度どこかで使うと使わない、という選択肢が割と取りづらいので、ならいっそむしろ超究極パフォーマンスのasync/awaitを提供すればそれで全部解決なのである。いう長ったらしい前置きにより、つまり超究極パフォーマンスのUnityのasync/await統合を提供するライブラリを作りました。場所は(面倒くさいので)UniRxに同梱です。いうわけでなんと久しぶりにUniRxも更新しました......!(主にReactivePropertyが高速になりました、よかったよかった。アセットストアに既に上がっています。UniTask何ができるか、について。並列実行して待機、みたいなのも簡単に書ける。UniTaskVoidとなっています。何が出来るのかと言うと、ようはコルーチンの完全な置き換えが可能です。async/awaitがあります、っていう道具立てだけだと、何もかもが足りないんですね。機能するようにフレームワーク側でサポートさせてあげるのは必須なのですが、前述の理由(?)どおり、Unityはサポートする気が1ミリもなさそうなので、代わりに必要だと思える全てを提供しました。Taskを投げ捨てよ目の付け所がいかれているので、Taskを投げ捨てることにしました。Taskってなんなの?というと、asyncにする場合戻り値がTaskで強要される、という型。究極パフォーマンスの実現として、このTaskがそもそも邪魔。なんでかっていうと、歴史的経緯によりそもそもTaskは図体がデカいのです。異様に高機能なのは(TaskSchedulerがどうだのLongRunningがどうだの)、ただたんなる名残(或いは負の遺産)でしかない。Frameworkのバージョンアップの度に)コードパス的に小さくはなっていったのですが(async/awaitするためだけには不要な機能がてんこ盛りなのだ!)、もういっそ全部いらねーよ、という気にはなる。7.0からasyncの戻り値を任意の型に変更することが可能になりました。C#に書いてありますが、Builderを実装することにより、なんとかなります。いうわけで、UniRx.Asyncでは軽量のTaskであるUniTaskと、そのためのBuilderを完全自前実装して、Unityに最適化されたasync/awaitを実現しました。2018の近いバージョンではC#のバージョン上がりそうな気配なので、先取りするのは悪くないでしょう。PlayerLoopUniRx.AsyncはUniRxに依存していません。そのため、GitHubのreleasesページではUniRxを含まないパッケージも提供しています。併せて使ったほうがお得なのは事実ですが、なしでも十分に機能します。UniRxではMainThreadDispatcherというシングルトンのMonoBehaviourにMicroCoroutine(というイテレータを中央管理するもの)を駆動してもらっていましたが、今回スタンドアロンで動作させるため、別の手段を取りました。それがPlayerLoopです(詳しくはテラシュールブログの解説が分かりやすい)。これをベースにUpdateループをフックして、await側に戻す処理を仕掛けています。SynchronizationContext。いうと、そもそもUnityの非同期って、C++のエンジン側で駆動されていて、C#のスクリプティングレイヤーに戻ってくる際には既にメインスレッドで動くんですよね。retunもそうですね、PlayerLoop側で処理されている。ようするに、本来SynchronizationContextすら不要なのです、全てメインスレッドで動作するので。ASP.NETなど諸々の事情を吸収するために存在していたわけですが、Unityだけで考えるなら完全に不要です。他のものにはないフレーム毎に駆動することと、本体がC#ではなくC++側にあるということが大きな大きな違いです。async/awaitやTask自体は汎用的にする必要があるため、それらの吸収層が必要(SynchronizationContext)なわけですが、当然ながらオーバーヘッドなので、取り除けるなら取り除いたほうが良いでしょう。そのために、UniTaskの独自実装も含めて、全てのコードパスを慎重に検討し、不要なものを消し去りました。UniTaskはどちらかというとJavaScript的(シングルスレッドのための非同期の入れ物)に近いです。Taskは、そうした非同期の入れ物に加えてマルチスレッドのためなどなど、とにかく色々なものが詰まりすぎていて、あまりよろしくはない。非同期とマルチスレッドは違います。JobSystemを使ったほうが良いので、カジュアルな用途以外(まぁラクですからね)ではマルチスレッドとしてのTaskの出番は少なくなるでしょう。嬉しいこととして、スレッドを使わないのでWebGLでもasync/awaitが完全に動作します。async/awaitもう結論が出ていて、async/await一本でOK、です。Rxには複数の側面があって、代表的にはイベントと非同期。そのうち非同期はasync/awaitのほうがハンドリングが用意です。コルーチンによるフレームベースの処理に関してはUniTask.DelayやYieldが解決しました。いうように分離されていくと思われます。Unity正直なところ私は別にUnityがC#スタンダードに添わなくてもいいと思ってるんですよね。繰り返しましが、Unityの本体はC++の実行エンジンのほうで、C#はスクリプティングレイヤーなので。C#側が主張するよりも、C++に寄り添うことを第一に考えたほうが、よい結果がもたらされると思っています。C#ゲームエンジンでないとならないですが、商業的にはほぼ全滅であることを考えると、Unityぐらいの按配が実際ちょうどいいのだろうな、と。理想もいいんですが、ビジネスとしての成功がないと全く意味がないので。最高なわけですが(そしてECSなんてそもそもオブジェクト指向ですらなくなる)、さて、Task。UniTaskの有用性や存在意義については、よくわかってもらえたと思います!そのうえで、それを分かったうえでもノンスタンダードな選択を取るべきなのか論は、それ自体は発生して然りです。7.0から追加された)に置き換え祭りは発生するでしょう。実際async祭りで組むと、「同期で動くTask」がどうしても多く発生してしまい、無駄なアロケーション感半端ないので、ValueTask主体のほうがよい。2.1ではValueTaskにIValueTaskSourceという仕掛けが用意されて、これは何かと言うと、やっぱりasync/awaitの駆動においてTaskを無視するための仕組みです(現状はSystem.IO.Pipelinesというこれまたつい先週ぐらいに出た機能のみ対応)。別にUnityだけじゃなくて通常の.NETでもTaskはオーバーヘッドと認識されているのだ......。なんというか、そう、そもそもC#本流ですら割と迷走しているのだ......。存在すると思っているStandardなんてもはやないのだ......。てわけで、別にUniTask、いいんじゃない?とか思ってしまいますがどうでしょう。それはさすがにポジショントークすぎにしても。ようはポリシーとして、asyncで宣言した際に、TaskにするかValueTaskにするかUniTaskにするかを迫られます。逆に言えばそれだけです。あれ、意外と人畜無害。意外と人畜無害なのです。よし、なら、とりまやってみるか。いいんじゃないかな?別に最悪、一括置換で戻したり進めたり割と容易なので。あと、ちなみに、UniTaskがUnityでデファクトスタンダードになれば、尚更迷う必要性はなくなるので、むしろ是非みんなでデファクトスタンダードまで持っていきましょう:)まとめ非同期革命の幕開け!そもそもこれぐらいやらないと世論は動かない、というのもあるので、フルセットでどーんと凄い(っぽい)(実際凄い)のを掲示することにはめちゃくちゃ意味があります。UniTaskが流行っても流行らなくても、この掲示にはめちゃくちゃ意味があるでしょう。UniRx.Asyncが何を実現したかを理解することは非常に重要です、教科書に出ますよ!それと、UniRx全然更新していなくてごめんなさい、があります。今回、ReactivePropertyのパフォーマンス向上を(ようやく)入れたり、今後はちゃんと面倒みていくのでまたよろしくおねがいします。