MinimumAsyncBridgeの現状について。 どのくらい真っ当に動いているか。

ちなみに、7月に書いたときには自分の個人アカウントのリポジトリにコードを置いていましたが、 今は、[会社アカウント]((https://github.com/OrangeCube/)の方に移っています。

実装状況

背景でも説明した通り、Runメソッド(別スレッドで処理を開始する)は実装していません。 ここはUnityコルーチンとかRxとつないで使う想定です。

一方で、Minimumと言いつつ、DelayWhenAllWhenAnyは実装しました。

これで、実プロジェクトに組み込んで使っていますが、ほぼ要件足りています。 async/awaitを使いたい動機が主にサーバーとの通信(I/O非同期)で、スレッド処理(CPU非同期)をあまり必要としていないという背景はありますが。

4か月使ってみて、MinimumAsyncBridge由来の問題に当たって困ったことはほぼないですし、 困ったのはだいたい実装ミスで、現在では修正済みです。

IteratorTasksからの移行

今開発中のプロジェクト、7月までは非同期処理にIteratorTasksを使っていました。

IteratorTasksも「Taskもどき」なので、この移行作業もそこまでは苦労しませんでした。必要なのは、

  • 名前空間を IteratorTasks から System.Threading.Tasks に変える
  • IEnumeratoryield returnTaskawait に変えて回る

を地道にやること。それなりに地道な作業なんですが… MinimumAsyncBridgeの移植やらバグ修正をしながらの同時作業で1・2週間ほどで完了。

もし、IteratorTasksを使っていただけてる方で、この地道な作業をするほどの時間は取れないという場合があれば、 IteratorTasksに対するawaiter実装もあるので、これが使えるかも。

本家 Task の完全下位互換

Runメソッドみたいに意図して実装していないものを除いて、 本家Taskクラスと互換性があります。

要するに、完全下位互換。 MinimumAsyncBridgeを使って動けば、 .NET 4.6以降では本家Taskを使って100%動きます (逆は、実装していないメソッドを避けないとダメ)。 CompletedTaskとか、.NET 4.6での追加分も実装してあるのでこの辺りを使うと.NET 4.5では動かなくなりますが、それを避ければ.NET 4.5でも動きます。

ちなみに、型フォワーディングという仕組みを使っていて、

  • .NET 3.5 (Unity)向けライブラリでMinimumAsyncBridgeを使う
  • .NET 4.5向けライブラリで本家Taskクラスを使う
  • これらのライブラリを混在させて、.NET 4.5向けアプリを書く

というようなこともできます。.NET 4.5向けアプリからMinimumAsyncBridgeを使うと、 本家Taskクラスに転送されます。

実際、今作っているゲームは.NET 3.5と4.5混在です。

  • ちょっとしたデバッグをコンソール アプリで
  • ボット使った動作テストしたいときにボットをコンソール アプリで
  • 可視化したいデータがあったときにWPFでGUIをちょろっと
  • データの編集ツールはWPF製
  • (基本、サーバー側は外注なので他社、かつ、PHPなものの)一部、CPU酷使しそうな機能はC#で実装

これらのプログラムは全部、Unity上で動かすゲームのロジックを共有しています(ソースコード レベルでの共有じゃなくて、同じDLLを参照)。

ちなみに、現在は、これら全部を.NET 4.6にバージョンアップ済みです。

既知の制限: .NET 4では使えない

.NET 3.5と4.5以上では問題なく動きますが、.NET 4では動きません。

おそらくピンポイントに.NET 4が必要になる(.NET 4は使えるけど4.5以上へのアップグレードはできない)場面は少ないと思いますが、一応ご注意を。

これは、.NET 4に、async/awaitはできない中途半端なバージョンのTaskクラスが存在するため、 型フォワーディングしづらいせいです (新規クラスを足すのは簡単。既存クラスにインスタンス メソッドを追加するのは無理)。

NuGetパッケージ

NuGetパッケージ化してあって、

  • .NET 3.5 のプロジェクトからこのNuGe パッケージを参照すると、バックポーティング実装が参照される
  • .NET 4.5 のプロジェクトからだと、型フォワーディング実装が参照される

という挙動をします。とりあえず、NuGet経由で参照するとわずらわしい設定不要で、.NET 3.5と4.5の混在ができます。

UniRx向け型フォワーディング

同じような仕組み、UniRxに対しても使えます。つまり、

  • Unity向けライブラリではUniRx使う
  • .NET 4.5向けライブラリでは本家Rxを使う
  • これらのライブラリを混在させて使う

とか。とりあえずこの作業やって、Pull Request は出してあったりします。

やったことは、

  • 名前空間が UniRx になっているところをSystem.Reactiveに戻す
  • 意図して本家Rxとは実装変えていた部分を元に戻す(FirstAsyncFirstとか)
  • 別途、型フォワーディング用のライブラリを実装
  • 元々のUniRx利用者に影響が出ないように、#if分岐

とかです。

既知の制限: C# 6.0 がらみ

MinimumAsyncBridgeというかIL2CPP側の問題なんですが、 IL2CPPを使う場合(つまり、iOSで実行する場合)、ちょっとだけC# 6.0で使えない構文があります。

現状、以下の構文はIL2CPPでのビルドに失敗、あるいは、iOS実行時に実行時例外を起こします。 async/awaitの問題というか、だいたいは例外処理がらみの問題です。

iOS実行・Android実行

iOSでの実行は、IL2CPPを使えば動きます。

IL2CPPのバグをいくつか踏み抜いて、Unity 5.2.2では動かなくなっていたりしましたが、 Unityへのフィードバックの結果、Unity 5.2.3では直っています。

(Unity 5.2.2の時は、ビルドで失敗していました。 ビルドまで成功したら、実行時エラーが出たことはこれまでどのバージョンでもありませんでした。)

ちなみに、Android実行では1度も問題は出ていません。

本家Taskクラスらの劣化

Runメソッドを実装していないなど、意図的にそうした部分の他に、いくらか残念な部分があります。

  • 一部、多少効率悪いはず
  • 例外のスタックトレース紛失

効率

TaskCompletionSourceクラスとかawaiterの実装などはほぼベタに本家からの移植なので、そんなに差はないはずです。

問題はDelayWhenAllWhenAnyの実装。 本家はこの辺りを結構大掛かりな実装をしていて移植する気になれなかったので、 MinimumAsyncBridgeでは別実装しました。 この辺りは、本家ほど最適な実装はできないんで、さすがに効率ちょっと悪いはず。

  • Delay: System.Threading.Timerで実装
  • WhenAll/WhenAny: 複数のTaskをリスト管理
    • 本家実装だと同時実行するタスク数がむちゃくちゃ多くてもメモリ食わない実装になってるらしいけど、こっちはしっかりタスク数分のメモリを食う。

スタックトレース

本家Taskを使ったasync/awaitの何がすごいって、 スレッド間でスタックトレース情報を伝搬させて、スタックトレースを追えるようにしていることなんですが。

ここはさすがに移植できなかったというか、移植が不可能だったりします。 この機能の実現には.NET 4.5で追加されたSystem.Runtime.ExceptionServices.ExceptionDispatchInfo.Captureが必須です。 .NET 3.5向けのバックポーティングはできません。

なので、デバッグは本家Taskよりも少し大変になります(非同期メソッドの呼び出し元が分からなくなったりする)。

まとめ

MinimumAsyncBridge、 実装し始めた当初の予想よりもだいぶあっさりと完成。

  • .NET 3.5/4.5混在でも使えます
    • .NET 4では使えません
  • ほとんど問題起きず、少ない修正で安定してしまいました
  • iOS(IL2CPP)でも動きます
    • Unity 5.2.2でIL2CPPのバグで一瞬動かなくなっていたものの、5.2.3で修正されました
  • Androidは余裕で動きます
  • 本家Taskの完全下位互換です
    • MinimumAsyncBridgeで動けば、本家では100%動く
    • 一部、多少性能的に劣る
    • スタックトレース紛失で少しデバッグが大変