2018年を振り返る

毎年恒例ということで、今年も振り返ります。だいたい30日に書いてるのですが、理由は12月30日は私の誕生日なので色々ちょうどよいかな、と。いよいよ35歳なので、例のあれ、35年定年説になりました。そのへんどうでもいい外れ値をひた走ってるので一般論はあんま関係ないんですが、体力は落ちてる実感ありますね!肥ったし。文章はどんどんてきとーになってくし。

と、いうわけで、今年は客観的には激動の年です。会社辞めて会社作って会社作って。私生活でも色々あり、イベントが多くて中々どうして落ち着かなかった年です。そのため成果という点では不完全燃焼が否めないですね、どうしても集中しきれないし時間も上手く捻出できないし。GitHubの草生やしで考えれば、もう全然すっかすっか。その中でUniRx.AsyncやMagicOnion2など、今年もちゃんと大きめの成果を出せたのは、意地です。特にUniRx.Asyncの開発はしんどかった……(MagicOnion2のほうはCysharpとしての時間を使えたので良かったんですが、UniRx.Asyncの開発のための時間捻出はめっちゃ厳しかった)。まぁ、ダラダラ草生やすことをKPIにするよりは、一発ホームランをKPIにしたほうがエンジニアの脳トレ的にも随分かいいんじゃないでしょーか?

ここ数年は毎年、C#を書く技量が向上してていいわー、と言い続けてるんですが、今年も随分と向上しました!特に大きかったのはUniRx.Asyncの開発で、これのためにasync/awaitやTask周りの生態系を全部自前実装したので、曖昧な理解だった、ということに気づいてすらいなかったものも、全て完全に理解したので、私自身の能力の向上としてかなり大きいですね。車輪の再発明は良いものです。

きちんと最前線のC#を書けている自信がありますし、対外的な証明もできているので、能力的な意味では老害とはまだ遠そうでいいんじゃないでしょーか。色々な言語に手を出して成長、ってのも悪くはないでしょうが、一つの言語を集中的に深掘りするというのもまた成長の道かと思います。中途半端な深掘りだと言語固有の話になって応用がー、みたいなところがなくもないのですが、徹底的に深掘りすりゃあ、逆に言語固有じゃなくなって、応用が効いてくる領域に入るのです。もしなんとなく成長の限界を感じて他の言語やったほうがいいかな、とか思うのだとしたら、多分、全然深掘りが足りないじゃないかしら?と、思ったりね、します。実に上から目線ですが!

お仕事

先に仕事の話を考えると、株式会社グラニを退任しました。私だけではなく皆バラバラになっているので(もちろんマイネットさんのほうで引き続きタイトル開発にあたっているメンバーもいます)、グラニという会社の始まりから終わりまで、ですね。結末として、悪くはない(退職時の未払いなどももちろんありませんし、エンジニアメンバーは他社に転職した人も、みな良いところに移れているので、グラニという会社が経験としてもキャリアとしても、良いものを提供できたのではないかなー、と思ってます、まぁ役員としてはこれが最後の仕事の成果という形になるのでそう思わせてください……!)ですが、もちろん、最良ではない、です。

CTOとしてどうだったかというと、会社として一点突破な凡百じゃない他にない個性を作れたし、悪くないところまで突き進めた、という点ではよくやれてはいますが(別に「素で」やってるわけじゃなくて割としっかり戦略組んでやっての結果なのでそれなりに大変なんですよ!)、最良のエンディングではないという結果をもって、ベストじゃあないでしょうね。実際反省点はめちゃくちゃ多いです。世の中、結果が全てで、一般論のハンマーを叩き返すには結果を出していかなきゃいけないので、今回の5年間の結果では一般論に反逆はできないんで、次はもっとうまくやる。という決意もあります。

退任後にはNew World, Inc.を設立しました。設立じゃない道も模索していたのですが、うまくまとまらなかったので、とりあえずやったことないしやってみっか、と。なんで株式会社なのかというと、とりあえず作ってみたかったから、以外の理由はない、です。次の会社のスタートまでの間(4ヶ月ぐらい)は、この会社名義、といってもほぼ個人事業主として仕事していました。一応会社としてのビジネスプランも考えてはいたのですが、時間的なものもあって結局ほとんど個人事業主的な働き方に終始しました。

こちらは死ぬほど反省点ありますね……。あまり表明するのもアレですが、多分、まぁ、請負で作業するの自分には向いてないんだとは思います……。それ以外にも単純にスキル不足があって、自分が組んできた環境ではなく自由度もそう高くない中でのパフォーマンスチューニング、みたいなことをするための手札があんまなかったですね。こういうスキルって、多分テクニカルサポート(それも高額なプレミア契約の)の人が持つスキルなんでしょうけれど、明らかに自分の手札にはなかったのを自覚しました。ここはもう能力不足だし、あったほうがいいのは間違いないんで、来年は伸ばしていこうかな、とは思ってます。

グラニでのトラブルシューティングは、徹夜で張り付いて空いてそうな時間にサーバー一時的に止まるの強行でダンプ取ったり、本番環境データベース相当のをコピーして好きに実験したり、既に豊富に用意しているモニタリング系システムにクエリ書いたり追加したり、そもそも根本からライブラリを自作のものを開発して取り替えたりと、権限に甘えきったことやってたんで、まぁ世の中そんなイージーモードなわけないぞ、と。そりゃそうだ。会社としてはイージーモードな世界を作るのが大事で、個人スキルとしてはハードモードな世界でやりきれる能力をつけるのが大事。両面から頑張っていきたい。

そして、株式会社Cysharpを設立しました。設立に関する話は←の記事と、Social Game Infoでのインタビューを読んでいただければなのですが、まず、この座組のインパクトはめっちゃあったかな、と!社名もそうですが!大きなことがやれそうな予感があります。来年はそうした予感を実現していくという、チャレンジの年です。会社としてもそれなりな規模にはしていきたいと思っているので、そのへんもやっていければ。

引き続きゲーム関連で勝負をかけるわけですけれど、ゲーム関連から攻めるのがC#にとって一番、あるいは唯一芽があるから、というのも大きくて。私はインターネットで育ってきたので、ウェブに話題が少ない言語や環境って嫌なんですよね、だからエンタープライズで採用伸びてるとか世界的には数字は良いんだとか、そういうの興味なくて。目の前のインターネットの世界で話題になり誰もが使う言語、であって欲しい。C#が。誰もが使い、話題にし、エコシステムが形成され、若い人がどんどん使う。そこへ向かうには、ゲームやUnityと絡めていくことが唯一の道だと思っています(個人的にはXamarinでは無理でしょう、と思ってるので)。そしてそこに対して貢献したいのです。それはMicrosoftやUnityの「外の人」だからこそできることでもある。

C#

UniTask(UniRx.Async)のリリースによるコンセプト実証大規模アップデートによる真の実用化。これはNew Worldとして動き出してちょい過ぎぐらいから作成に着手しました。私の中でこれのリリースには大きな意味があって、New World, Incとしての名刺、つまるところ私自身の自信が欲しかったのです。特にUnity関連においてUniRxは前の世代の話なので、今の世代で絶対の自信を持って薦められるものが欲しかった。そういうものがあると自分にも自信ができるんで、交渉も強気に迫れますからね。

UniTaskはUnityに最適化したasync/await生態系の再発明です。これやりきれる人って世の中いないんで、成果として世の中に存在するのはめっちゃレアなんじゃないでしょーか。と思える程度にはいい感じだと思います。しかし例によってまだバグや機能改善がかなり残っているのに、いったん放置が始まっていて、これは本当に私の悪癖ですね……。来年の抱負は放置しない、です。ほんと。ほんと。

もう一個がMagicOnion v2のリリースで、これはグラニでのやり残したことの一つの消化、という意味合いもあります(技術的にはもう一つやり残したことありますけれど、「まともな」UIライブラリの作成とプラクティスの構築とか)。そして、Cysharpで掲げる「C#大統一理論」のキーパーツですね。応用事例をどんどん作っていきたいます。

また、MagicOnionはリポジトリをCysharp/MagicOnionに移しています。これはメンテしっかりやっていくぞ、の現れですね!他にUniRxやMessagePack for C#などもorgnaization(Cysharpじゃなくて中立的組織)に移したいなあ、とは思ってます。そうした継続的メンテナンス体制を作って、永く行き続けていくものになっていければいいなあ、と。まあカウボーイエンジニアから会社経営者になったわけで、そのへんも世の中によりよい形を、ということで。

パフォーマンスの追求は引き続きやっていきたいテーマで、一旦のまとめをCEDEC 2018で最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭すると題して講演しました。こういうのってどのへんまでDeepでDopeに書けばいいのか難しくて、浅瀬ちゃぷちゃぷなんちゃうんちゃうんな思いと戦いつつ、実体験ベースってのもあり良いちゃあ良かったんじゃないでしょうか。

Unity ECSはもっと力入れてやりたかったんですが、ほとんど出来なかったですね。LTドリブン開発だー、と思ったけれどロクにできずMemory Management of C# with Unity Native Collectionsでお茶濁しした程度だったので……。物理エンジンと絡めて、やりたいネタが2年ぐらい前からあって、ECSの登場でまさに最適なプラットフォーム!これでちゃりんちゃりんする!と考えてたのに何も実装できず一年が終わるとは……。来年こそリベンジします。

漫画/音楽/ゲーム/その他…

今年感動したゲームはDetroit: Become Human一択ですねえ、もう全く悩まず。映像も音楽もシナリオもシステムも、あらゆる点で監督(デヴィッド・ケイジ)の神経質そうな(ほんと!)目が行き届いていて、完璧。ただの雰囲気ゲーにならずゲームとしてもちゃんと面白く仕上がっているので、非の打ち所がない(リプレイが面倒くさくてエンディングコンプがダルい問題はありますが、一周+αの体験だけで十分価値あるんで良いんじゃないでしょーか)。

漫画はうめざわしゅん - えれほんがとても良くて、三話(+1)入った短編集で二話目は無料で読めます。とにかくまぁ漫画が上手い。まぁあと古いですが今年読んだんで内田 春菊 - 目を閉じて抱いても。全然古くないというかむしろ今の時代のほうが共感できるんちゃうんちゃうん、と(といっても1994-2000だとそこまで古くもないのかー、いや、古いかー)。

音楽は、今年まぁまぁケンドリック・ラマーの記事を目にしたから(来日で一悶着あったからかしらん)、というわけでもないですが最新作のDAMN、ではなくてその前のTo Pimp A butterflyをよく聞いてました。特に一曲目のWesley's Theory。もともとこれ作曲してるFlying Lotusが好きというのもありますが、Flying Lotusの手掛けた中でも傑作と思ふ。アルバム全編を貫くストーリーも重たく、比喩も強烈で、言葉の強さを心底叩き込んでくる。そりゃ心震えますよ!

映画は、これも古いですがNetflixで見たニコラス・ウィンディング・レフン - ドライヴが良かった。映像美とバイオレンス!まぁーもうとにかく最高に格好良い。こりゃすげえ、と思わず監督の大ファンになって初期作のプッシャー三部作、これはまぁまぁだったんですが、最新作のネオン・デーモンが……。設定も良い。映像も良い。プロットも悪くない。映画としてはつまらない。というしょっぱい代物に……。脚本と構成が悪いんかなー、もう少しうまくやりゃあ、めっちゃ良くなったはずなのに感で超もったいない……。設定と映像は本当に好みなだけに、あうあうって感じ。何れにせよ次回作あったら見るよ!と思ったら、来年(2019)にAmazon Prime VideoのシリーズとしてToo Old to Die Youngというドラマを撮ってるそうで。日本に来るのかな、Netflixだったら絶対来るはずだけどAmazon Prime Videoだとどうなんでしょ。

Hajime Kinoko写真集「Perfect Red」は、写真集はもちろん、個展とショーも見に行きましたが、圧倒的な空間で。ショーだと、人間の表現として、静と動の中間のような世界なんですよね。例えば、静が彫刻、動がバレエのようなものだったりだとすると、その間。ほとんど止まっているのだけれど、手の動き、顔の表情、そして縄が皮膚に言葉を与えていて。表現としては未成熟というかアングラ感が拭えないですが(決して悪いことではなくそれはそれでいいんですが)、もっと表に出れば出るほど洗練されていくのでは、というわけで来年も見たいですね。

来年は

とにもかくにもCysharpをしっかり始動させます。会社の理念がC#とOSSを中心に、というのもあるので、技術のコミュニティ貢献も引き続き、ですね。そしてメンテやIssueを放置しないという抱負は、ただの意気込みじゃなくて組織的な解決となるよう、具体的に動いていきます。

技術的には、今年は色々な言い訳がありますが、チャレンジがなかったなー、というのは否めないです。UniRx.AsyncもMagicOnion2も延長線上ですから。時間がない中で一定の成果を出すことが必須という状況下だったのでしゃーなし、という言い訳もありますが、来年こそは今までの延長線上にない別のことも手掛けたい、と毎年言ってますが、今年も思います。とりあえずとにかくUnity ECSは絶対やりますから……!

ともあれ、Cysharpの活動にご期待下さい。人も募集ちゅ(します、そろそろ)。です。そろそろお問い合わせフォームぐらいはつけたい。

MagicOnion v1 -> v2リブート, gRPCによる.NET Core/Unity用ネットワークエンジン

先にCygames Engineers' BlogでMagicOnion – C#による .NET Core/Unity 用のリアルタイム通信フレームワークとしてリリースを出しましたが、改めまして、MagicOnionというフレームワークを正式公開しました。

GitHub - Cysharp/MagicOnion

MagicOnionはAPI通信系とリアルタイム通信系を一つのフレームワークで賄う、というコンセプトを元に、前職のグラニで「黒騎士と白の魔王」の開発において必要に迫られて捻り出されたものでした。

「黒騎士と白の魔王」gRPCによるHTTP/2 - API, Streamingの実践 from Yoshifumi Kawai

で、今更気づいたのがMagicOnionって正式リリースしてなかったんですよね、このブログでも↑のような形でしか触れていなくて、公式ドキュメントも貧弱な謎フレームワークだったという。今回Ver2って言ってますが、その前はVer0.5でしたし。まぁここでは便宜的にv1と呼びます。

何故に正式リリースまで行かなかったかというと、リアルタイム通信部分が微妙だったから。↑のp.39-40で説明していますが、Unary + ServerStreamingという構成で組んだのが、かなり開発的に辛かったんですね。時間的問題もあり強行するしかなかったんですが、ちゃんと自分が納得いく代案を出せない限りは、大々的には出していけないなあ、と。

その後すったもんだがあったりなかったりで、プレーンなgRPCでリアルタイム通信を組む機会があって、↑の時に考えていたDuplexStreaming一本で、コマンドの違いをProtobufのoneofで吸収する、という案でやってみたのですが、すぐに気づきましたね、これ無理だと。UnaryはRPCなのですが、Duplex一本はRPCじゃないんで、ただたんにoneofをswitchしてメソッド呼び出す、だけじゃ全然機能足りてない、と。

ただまぁコネクション的にはDuplex一本案は間違ってなさそうだったので、その中で手触りの良いRPCを組むにはどうすればいいか……。と、そこでMagicOnionをリブートさせるのが一番手っ取り早いじゃん、というわけで着手したりしなかったりしたりしたのでした。その間にCysharpの設立の話とかもあり、事業の中心に据えるものとしても丁度良かったという思惑もあります。

早速(?)Qiitaでも何件か紹介記事書いてもらいました。

v1 -> v2

Unary系(API通信系)はほとんど変わっていません。それは、v1の時点で十分に高い完成度があって、あんま手を加える余地はなかったからですね。ただしフィルターだけ戻り値をTaskからValueTaskに変えています。これはフィルターの実行はメソッド実行前に同期的にフック(ヘッダから値取り出してみるとか)するだけ、みたいなものも多いので、TaskよりValueTaskのほうが効率的に実行できるからですね。

元々フィルターを重ねることによるオーバーヘッドを極小にするため、純粋にメソッド呼び出しが一個増えるだけになるように構成してあったのですが、更により効率的に動作するようになったと思います。

SwaggerのUIを更新するのと、HttpGatewayの処理を効率化するのが課題として残っているので、それは次のアップデートでやっていきます。

また、Unity向けにはコードジェネレート時にインターフェイス定義でTaskをIObservableに変換していたのですが、今のUnityは.NET 4.xも使えるということで、インターフェイスはそのまま使ってもらうようにしています。Taskのままで。

StreamingHub

の、導入に伴って、v1でリアルタイム通信系をやるための補助機構である StreamingContextRepository を廃止しました。StreamingContextRepositoryは、まぁ、正直微妙と思っていたのでなくせて良かったかな。決して機能してないわけではないのですけれど。

代わりのコネクションを束ねる仕組みはGroupという概念を持ってきました。これはASP.NETのWebsocketライブラリであるASP.NET Core SignalRにあるものを、再解釈して実装しています。

MagicOnionのGroupの面白いところは、裏側の実装を変えられることで、デフォルトはImmutableArrayGroupという、MORPGのようなルームに入って少人数で頻繁にやり取りするようなグルーピングに最適化された実装になっています。もう一種類はConcurrentDictionaryGroupという、こちらはMMORPGやグローバルチャットのような、多人数が頻繁に出入りするようなグルーピングのための実装です。更に、RedisGroupというバックエンドにRedisのPubSubを置いて複数サーバー間でグループを共有するシステムも用意しています、これはチャットや、全体通知などに有効でしょう。

また、GroupにはInMemoryStorage[T]というプロパティが用意されていて、グループ内各メンバーに紐付いた値をセットできるようにしています。これは、通信のブロードキャスト用グループの他に、値の管理のためにConcurrentDictionary[ConnectionId, T]のようなものを用意してデータを保持したりが手間で面倒くさいんで、いっそグループ自体にその機能持ってたほうが便利で最高に楽じゃん、という話で、実際多分これめちゃくちゃ便利です。

まとめ

というわけで、リブートしました!最初チョロいと思ってたんですが、割とそんなことはなくて、この形にまとめあげるまではそれなりに大変でした……。の甲斐もあって、今回のMagicOnionはかなり自信を持って推進できます。以前はそもそもgRPC本体をフォークして魔改造したり、というのもあったのですが、今は公式ビルドを使えるようになったのでUnity向けにも良い具合になってきています。

MagicOnion2の内容は、(v1を)実際に使ってリリースした後の反省点が盛り込まれているので、そういう点で二周目の強みがあります。最初からこの形で出すのは絶対にできないであろうものなので、しっかりと経験が活かされています。実プロダクトで使って初めて見えるものっていっぱいありますからねー。とはいえv1はv1で大きな役割を果たしたと思いますし、まぁあと自分で言うのもアレですが「黒騎士と白の魔王」が証明したこと(gRPCがUnityでいけるんだぞ、という)ってメチャクチャ大きかったなあ、と。

CysharpとしてもMagicOnion、使っていきますし、ほんと是非是非使ってみてもらえると嬉しいです。コードジェネレーターもついにWin/Mac/Linux対応しましたので(まだ微妙にバグいのですが年内か、年明け早々にはなんとかします)、ガッツリと使っていけるのではないかとです。

UniTask(UniRx.Async)から見るasync/awaitの未来

C# Advent Calendar 2018大遅刻会です。間に合った。間に合ってない。ごめんなさい……。今回ネタとして、改めてコード生成に関して、去年は「動的」な手法を解説した - Introduction to the pragmatic IL via C#ので、現代的な「静的」な手法について説明してみよう、と考えていたのですが、そういえばもう一つ大遅刻がありました。

7月にUniTask - Unity + async/awaitの完全でハイパフォーマンスな統合という記事を出して、リリースしたUniTaskですが、その後もちょこちょこと更新をしていて、内部実装含め当初よりもかなり機能強化されています。といった諸々を含めて、Unity 非同期完全に理解した勉強会で話してきました。

Deep Dive async/await in Unity with UniTask(UniRx.Async)

9月!更新内容の告知もしてなければ、この発表のフォローアップもしてない!最近はこうした文章仕事がめっちゃ遅延するようになってしまいました、めっちゃよくない傾向です。来年はこの辺もなんとかしていきたい。

と、いうわけで、予定を変えてUniRx.Asyncについて、というか、それだとUnity Advent Calendarに書けよって話になるので、UniRx.Asyncは独自のTask生態系を作っている、これは.NET Core 2.1からのValueTaskの拡張であるIValueTaskSourceに繋がる話なので、その辺を絡めながら見ていってもらえると思います。

Incremental Compilerが不要に

告知が遅延しまくっている間にUnity 2018.3が本格リリースされて、標準でC# 7.xに対応したため、最初のリリース時の注釈のような別途Incremental Compiler(preview)を入れる必要がなくなりました。Incremental Compiler、悪くはないのですが、やっぱpreviewで怪しい動きもしていたため、標準まんまで行けるのは相当嬉しいです。というわけで今まで敬遠していた人も早速試しましょう。

new Progress[T] is Evil

これは普通の.NETにも言える話なのですが、C#のasync/awaitの世界観では進捗状況はIProgress[T]で通知していくということになっています(別にAction[T]でよくね?そっちのほうが速いし、説はある)。進捗はReport(T value)メソッドで通知していくことになりますが、こいつは必ずSynchronizationContext.Post経由で値を送ります。これがどういうことかというと、Unityだとfloatを使う、つまりIProgress[float]で表現する場合が多いはずですが、なんと、ボックス化します。(If T is a value type, it will get boxed here.)じゃねーよボケが。アホか。これはオプションで回避不能なので、new Progress[T]は地雷だと思って「絶対に」使わないようにしましょう。

代わりにUniRx.AsyncではProgress.Createを用意しました。これはSynchronizationContextを使いません。もしSyncContext経由で同期したいならマニュアルでやってくれ。Unityの場合、進捗が取れるシチュエーションはメインスレッド上のはずなので、ほとんどのケースでは不要なはずです。

こういった、あらゆる箇所での.NET標準の余計なお世話を観察し、Unityに適した形に置き直していくことをUniRx.Asyncではやってるので、async/await使うならUniRx.Asyncを使ったほうがいいのです。標準のも、今の時代で設計するならこうはなってないと思うんですけどね、まぁ時代が時代なのでshoganai。

コルーチンの置き換えとして

コルーチン、或いはRxでできた処理は、改めて全部精査して、全てasync/awaitで実装できるようにしました。

Add UniTask.WaitUntil
Add UniTask.WaitWhile
Add UniTask.WaitUntilValueChanged
Add UniTask.WaitUntilValueChangedWithIsDestroyed
Add UniTask.SwitchToThreadPool
Add UniTask.SwitchToTaskPool
Add UniTask.SwitchToMainThread
Add UniTask.SwitchToSynchronizationContext
Add UniTask.Yield
Add UniTask.Run
Add UniTask.Lazy
Add UniTask.Void
Add UniTask.ConfigureAwait
Add UniTask.DelayFrame
Add UniTask.Delay(..., bool ignoreTimeScale = false, ...) parameter

概ね名前からイメージ付くでしょう、イメージ通りの挙動をします。こんだけ用意しておきゃほとんど困らないはず(逆に言えば、標準のasync/awaitには何もありません)

ちなみにSwitchTo***は、最初のVisual Studio Async CTP(お試しエディション)に搭載されていたメソッドで、すぐに廃止されました。というのも、async/awaitが自動でスレッド(SynchronizationContext)をコントロールするというデザインになったからですね。あまりにも最初期すぎる話なのでこの辺の話が残っているものも少ないのですが、ちゃんと岩永さんのブログには残っていたので大変素晴らしい。

UniRx.Asyncでは不要なオーバーヘッドを避けるため(そもそも特にUnityだとメインスレッド張り付きの場合のほうが多い)、自動でSynchronizationContextを切り替えることはせず、必要な場合に手動で変更してもらうというデザインを取っています。というか、今からasync/await作り直すなら絶対こうなったと思うんだけどなぁ、どうなんでしょうねぇ。ちょっとSynchronizationContextに夢見すぎだった時代&Windows Phone 7(うわー)とかの要請が強すぎたせいっていう時代背景は感じます。

Everything is awaitable

考えられるありとあらゆるものをawait可能にしました。AsyncOperationだけじゃなくてWWWやJobHandle(そう、C# Job Systemもawaitできます!)、そしてReactivePropertyやReactiveCommand、uGUI Events(button.OnClickAsyncなど)からMonoBehaviour Eventsまで。

さて、AsyncOpeartionなど長さ1の非同期処理がawait可能なら、そらそーだ、って話なのですが、イベントがawait可能ってどういうこっちゃ、というところはあります。

// ようするところこんな風に待てる
async UniTask TripleClick(CancellationToken token)
{
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    Debug.Log("Three times clicked");
}

コレに関してはスライドに書いておきましたが、「複雑なイベントの合成」をする際に、Rxよりも可読性良く書ける可能性があります。

Rxは「複雑なイベントハンドリング」を簡単にするものじゃなかったの!?という答えは、YesでもありNoでもありで、複雑なものは複雑で、難しいものは難しいままです。イベントハンドリングは手続き的に記述出来ない(イベントコールバックが飛び飛びになる)ため、コールバックを集約させて合成できるRxが、素のままでやるより効果的だったわけですが、async/awaitはイベントコールバックを手続き的に記述できるため、C#のネイティブのコントロールフロー(for, if, whileなど)や自然な変数の保持が可能になります。これは関数合成で無理やり実現するよりも、可読性良く実現できる可能性が高いです。

単純なものをasync/awaitで記述するのは、それはそれで効率やキャンセルに関する対応を考慮しなければならなくて、正しく処理するのは地味に難易度が高かったりするので、基本的にはRxで、困ったときの必殺技として手段を知っている、ぐらいの心持ちが良いでしょう

async UniTask TripleClick(CancellationToken token)
{
    // 都度OnClick/token渡しするよりも最初にHandlerを取得するほうが高効率
    using (var handler = button.GetAsyncClickEventHandler(token))
    {
        await handler.OnClickAsync();
        await handler.OnClickAsync();
        await handler.OnClickAsync();
        Debug.Log("Three times clicked");
    }
}

↑こういう色々なことを考えるのが面倒くさい。

Exception/Cancellationの扱いをより強固に

UniTaskでは未処理の例外はUniTaskScheduler.UnobservedTaskExceptionによって設定されている未処理例外ハンドラによって処理されます(デフォルトはロギング)。これは、UniTaskVoid、或いはUniTask.Forgetを呼び出している場合は即時に、そうでない場合はUniTaskがGCされた時に未処理例外ハンドラを呼びます。

async/awaitが持つべきステータスは「正常な場合」「エラーの場合」「キャンセルの場合」の3つがあります。しかし、async/awaitならびにC#の伝搬システムは、正常系は戻り値、異常系は例外の二択しかないため、「キャンセルの場合」の表現としてawaitされた元にはOperationCanceledExceptionが投げられます。よって、例外の中で、OperationCanceledExceptionは「特別な例外」です。デフォルトではこの例外が検出されて未処理の場合は、未処理例外ハンドラを無視します。何もしません。キャンセルは定形の処理だと判断して、無視します。

また、例外を使うためパフォーマンス上の懸念もあります。そこで、UniTask.SuppressCancellationThrowを使うことで、対象のUniTaskが例外の発生源であれば(throw済みで上の階層に伝搬されたものではない)、例外の送出ではなく、Tupleでの戻り値としてキャンセルを受け取り、例外発生のコストを抑えることができます。これはイベントハンドリングなどの場合に有用です、が、正しく使うことは内部をかなりのレベルで理解していないといけないため、ぶっちゃけムズい。ただたんにSuppressCancellationThrowを使うだけでパフォーマンスOKというわけにはいかんのだ。というわけで、どうしてもパフォーマンス的に困ったときのための逃げ道、ぐらいに思っておいてください。

UniTaskTracker

とはいえなんのかんのでTaskがリークしてしまったり、想像以上に多く起動してしまっていたりもあるでしょう。UnityのEditor拡張でトラッキングウィンドウを用意したので、すべて追跡できます。

image

こういうのRxにも欲しいわー。そうですね、なんか実装方法は考えてみようかとは思いますが一ミリも期待しないで待たないでください。

IValueTaskSourceでWhenAllを進化させる

.NET CoreのC#はTaskとValueTaskに分かれているわけですが、面倒くせーから全てValueTaskでいーじゃん、というわけにはいきません(なお、私の意見は全部ValueTaskでいいと思ってます、というのも使い分けなんて実アプリ開発でできるわけないから)。そうはいかない一番大きな理由はWhenAllで、このTaskで最も使われる演算子であろうWhenAllは、Taskしか受け取らないので、Taskへの変換が必要になってきます。せっかくValueTaskなのにモッタイナイ。じゃあValueTask用のWhenAllを作ればいいじゃん、というとそれも無理で、Task.WhenAllはTaskのinternalなメソッドに依存して最適化が施されているので、外部からはどうしても非効率的なWhenAllしか作れない仕様になっています(クソですね!)。

が、しかし、そもそもWhenAllってあんま効率的じゃなくないっすか?というのがある。と、いうのも、配列を受け取るAPIでも、まず保守的にコピーしてるんですよね。可変長引数でWhenAll(new[]{ foo, bar, baz })みたいに渡してもコピーされてるとか馬鹿らしい!あと、WhenAllの利用シーンでもう一つ多いのが WhenAll(source.Select(x => x.FooAsync()))のような、元ソース起点に非同期メソッドを呼んで、それを全部待つ、みたいなシチュエーション。なんかねー、別に配列作んなくてもいいじゃん、みたいな気になるんですよね。

と、そこでIValueTaskSourceの出番で、Task(ValueTaskですが)の中身を完全に自分の実装に置き換えることができるようになった、のがIValueTaskSourceです。よって、真に効率的なValueTaskに最適化されたうえで↑のような事情を鑑みたWhenAll作れるじゃん、って。思ったわけですよ。

そこでMagicOnionでは(UniRx.Asyncじゃないのかよって、IValueTaskSourceはUnityの話じゃないですから!)ReservedWhenAllPromiseというカスタムなWhenAllを用意してみました。

var promise = new WhenAllPromise(source.Length);
foreach (var item in source)
{
    promise.Add(item.FooAsync());
}
await promise.AsValueTask();

のように書けます。つまり何かと言うと、WhenAllに必要なのは「個数」で、個数が最初から確定しているなら、それを渡せばいいし、WhenAll自体の駆動に配列は必要ないので、随時Addしてあげてもいいわけです。これで、一切配列を使わない効率的なWhenAllが実装できました。めでたし。

他にも型が異なるTaskをawaitするのにValueTupleで受け取りたい、というのをTask.WhenAllを介さずにその個数に最適化したWhenAllを用意するとか、やりたい放題にめっちゃ最適化できるわけです。

と、いうのも踏まえて、(サーバーサイドC#における)アプリケーションのTaskの定義はValueTaskで統一しちゃっていいと思うし、そのかわりに幾つかの最適化したValueTask用のWhenAllを用意しましょう。というのが良い未来なんじゃないかなー、って思ってます。(このValueTask用のWhenAllのバリエーションはCysharpとして作ったらOSSで公開するので、こちらは期待して待っててください!)

まとめ

UniRx.AsyncナシでUnityにasync/awaitを持ち込んで使いこなすのはかなりの無理ゲーなので、よほどUnity以外で使い込んできた経験がある、とかでなければ、素直に使って頂ければと思います。また、そうでなくてもUnity向けに完全に作り直しているUniTaskの存在価値というのは、スライドのほうで十分理解してもらえてるのではとも思っています。

別にCLRの実装は至高のものだ!ってこたぁ全然なくて、時代とかもあるんで、後の世に作り直されるこたぁ往々にめっちゃある。Microsoftのハイパーエンジニアが練りに練ったものだろうがなんだろうが、永遠に輝き続けるコードなんてあんまなく、時代が経ちゃあどれだけ丁寧に作られたものでも滅びるんです。人間もプログラムも老化には逆らえない(WPFなんて何年前のUIフレームワークなんでしょう!)。というわけで、あんまり脳みそ固くせず、自分の意志で時々に見直して考えてみるといいんじゃないでしょうか。(古の)Microsoftよりも(現代の観点では)私のほうが正しい、とか自信持って言っておきましょう。

さて、UniRx.Asyncは(UniRxも)まだまだ完成しきってるとは言えない、のにドキュメント放置、更新放置で例によって半年ぐらい来てしまったのですが、その間は株式会社Cysharpを設立しましたであったり、MagicOnionのリブートであったり、結構わたわたしてしまったところがありなのですが、ようやく諸々落ち着いてきたので、また腰据えて改善に取り組んでいきたいと思います。まぁドキュメントが全然足りないんですけど(UniRx.Asyncの機能は、かなり膨大なのです……)。

C#的にも、自分でTaskの全域を見つめ直して作り直すという経験を通して得られたものも多かったので、今回の記事もそうですが、Unity関係なくasync/awaitを使っていく上で使える話は色々出せていければというところですね。ではまた次回の更新の時まで!次こそはすぐブログ書きますから!

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive