まずasync/awaitについて、 (Unityでない)通常のC#開発の場合がどうとか、 Unityで何が問題で使えなかったかとか、 Rxとの住み分けとかについて書こうかと思います。
C# 5.0: async/await の登場
C#界隈で非同期処理がらみの話題がホットになったのはだいたい2010年前後からです。
当時は、
- F# にコンピューテーション式による async ワークフローが登場
- 本家のRxが登場
- これらから少し遅れて、C# 5.0 (async/await)のプレビュー版が登場
という感じです。
最近になって、Rxを参考にした非同期処理ライブラリが、 Javaや JavaScriptなどで実装されていて注目されていますが、 その一方で、C#ではC# 5.0のasync/awaitが主流になりました。
理由はおおむね、
- async/await はプログラミング言語側のサポートが必要
- async/await が出ることが分かっていたので、それとは違うモデルの Rx の普及にはみな慎重だった
という辺りです。逆に言うと、
- C# のバージョンが古いUnityでは、いまだRxにかかる期待は大きい
- JavaScriptにもES7でasync/awaitが入り、(後述の住み分けはあるものの、それが合う場面では)async/awaitが主流になっていくと思われる
ということも言えます。
async/awaitとRx
今回、「Unityでもasync/awaitを使えるようにしたよ」という話をするわけですが、 これでRxはお役御免になるかというとそうでもなくて、Rxの用途は残ります。基本的には、
- async/await: pull型で、1つの値を取りに行く場合に有効
- Rx: push型で、ストリームデータを送ってもらう場合に有効
という住み分けになります。
例えば、サーバーからデータを受け取るような場合を想定すると、async/awaitがよさそうな場面は
- ログイン時、ユーザーデータを一括で取りたい
- ユーザーの行動時、即座に決まる範囲で行動の結果を返してもらいたい
Rxがよさそうな場面は
- 他のユーザーの行動で自分が受ける影響を、サーバーからpush通知してほしい
とかになります。
UnityでC# 6.0
Unityの問題は使っているMonoのバージョンが古いことで、確か2.8系(.NET 3.5/C# 3.0相当、オプション引数とか一部だけC# 4.0機能を先行取り込み)だったはず。
ですが、C#は、古いフレームワーク上でも最新の言語機能を使えたりします。 詳しくは「C#の言語バージョンと.NET Frameworkバージョン」で書いていますが、 ここで「2.0や3.0で動く」となっている機能は、コンパイラーさえバージョンアップすればUnity上でも使えます。
例えば、C# 6.0の新機能の大半は何もしなくても古いフレームワーク上で動きます。
Caller Info属性や、FormattedString
は、簡単なクラスの自作で動かせます。
async/await は結構重たいですが、頑張って実装すれば動かせます(これが本稿の主題)。
コンパイラーの差し替え
C# 6.0コンパイラーを使う方法は大きく分けると2種類あって、
- Unity が使っているコンパイラーを差し替えてしまう(実際にやっている人あり: Unity C# 5.0 and 6.0 Integration)
- ロジックの大部分をUnityの外でコンパイルしてしまう
となります。
うちは、どのみちロジックの大部分をUnityの外でも使う(データ編集用のデスクトップ アプリや、サーバー上で動かす)ので、 後者のやり方をしています。
ちなみに、C# プロジェクトのVisual Studio上の「ビルド後処理」設定で、 コンパイルしたDLLをUnity Assetsフォルダー以下にコピーするスクリプトを書いて使っています。
Unity に async/await 移植
Unity上でasync/awaitを使おうと思うと、Task
クラスの移植が必要になります。
IL2CPP登場以前だと、そもそもTask
クラスが内部で使っているいくつかのメソッド、
例えばInterlocked.CompareExchange<T>
とかが
iOS上(AOTコンパイル)で動かないという問題があって、移植は絶望的でした。
が、IL2CPPが安定してきた今、やってみたら案外あっさり移植できたという状態です。
導入文でも書いていますが、あっさり動きすぎて、コミットが少なすぎて非アクティブに見える…
まあ、「Minimum」という名前通り、Task
クラスのうち、async/awaitに必要な部分だけの実装になります。
例えば、Run
メソッド(別スレッドで処理を開始する)は実装していません。
これに関しては、他の非同期処理ライブラリ(Unityのコルーチンとか、Rxとか)とつないで使う想定です。
ちなみに、Rxに対するつなぎ処理も書いています。
IObservable
に対するawaiter実装になります。
まとめ
Unity の制限の多くは、Mono 2.8系のAOTコンパイルの制限に起因するものが多いです。 それが、IL2CPPが安定してきたことによって少し緩和しています。
C# 3.0に留まる理由もなくなっていて、async/awaitのバックポーティングもうまくいっています。 その移植したライブラリが以下のものです。