【非同期】あの日見た砂時計の名前を僕達はまだ知らない【Unity】

はいどーもニートと言えばじんたんですねこんにちは。
みなさんも手遅れにならないうちにめんまちゃんを探してくださいね。

というわけで本日は非同期処理を勉強していきたいと思います。

注*あくまでも勉強なので、僕が理解した範囲のことを書いているだけです。あんまり参考にしない方がいいかも。

コルーチンによる非同期処理

Unityで非同期処理と言えばコルーチンでしょう。
コルーチンは別スレッドで実行されているわけではなく、ふつーの処理と同じくメインスレッドで実行されています。
何で別スレッドで実行しないのかと言えば、Unityの設計がスレッドセーフではないことと関係しているようです。
例えば別スレッドでUnityEngine.Random.Rangeメソッドを呼ぶとエラーを吐きます。
コルーチン内でUnityEngineに関連した処理が呼ばれる可能性は十二分にあるので、コルーチンもメインスレッドで実行されてるんではないかなーと思います。

なぜコルーチンを使うのか

先ほどのコルーチンは「メインスレッドで実行される」と言いました。
とすると、Aという処理とBという処理を非同期で実行させたとしても、この二つの処理が最終的に完了する時間は同期的に実行した場合と変わらないはずです。
いやむしろ、処理を切り替える(コルーチンなら「yield return」のタイミングで切り替わる)コストがかかるので、むしろコルーチンを使う方が最終的に完了する時間は遅くなりそうです。
それでもコルーチンもとい非同期処理は一般的によく使用されています。

例えばWindowsXPなんかで何か重い処理をしていたとき、マウスポインタが砂時計マークに変わることを経験した方って多いと思います。
これは「今重い処理をしているから待ってくださいね」ということを視覚的に伝えるためです。
非同期処理をしていないプログラムが多かった昔は特に重宝したようですね。
しかしいつの頃からか(Windows8かららしいです)この砂時計マークが消えました。
これは「お前らいいから非同期処理で実装しろ!」ということかもしれません。

他の例も挙げます。
ゲームですと、例えばロード画面でよく非同期処理が用いられています。
「NowLoading…」みたいな文字と共にプログレスバーを表示するやつですね。
非同期にデータをロードしつつ、ユーザーには「今これだけデータを読み込んでいますよ」と教えているわけです。
もしこれが非同期処理をせず、同期的にデータをロードするだけだったなら、プログレスバーで進捗を伝えることはできません。
なぜなら、同期的に処理をおこなっている場合、「データをロードする」という処理が完了してからしか「プログレスバーを更新する」という処理ができないからです。
つまりユーザーがからすれば、画面が固まったかと思うと急にプログレスバーの進捗が100%になるわけですね。
これでは役に立ちません。
あるいは、ロード中に画面右下の方でキャラクターがチョコチョコ走る表示ってよく見かけますよね。
これも同期的に処理をしていたら無理です。

実際に作ってみる

ちゅーわけでデータのロード画面を作ってみましょう。
と言っても凝ったやつは作りません。
プログレスバーもどきを作り、そこにデータロードの進捗状況を示すだけです。
というわけでコードをドン。

想定としては、外部のサーバーに置いたアップデート用のデータをローカルに落としてきていて、その進捗状況をプログレスバーに表示している感じです。
実際に実行してみてください。
一つダウンロードが終わるたび、プログレスバーが一つ進みます(めちゃんこ早い回線使ってる人だと一瞬で終わる可能性もあるかも)。
まあ今回はかなり大雑把な進捗表示なのでコレくらいなら同期的に処理させても表示できるっちゃできる気がしますが、もっと細かく1%から100%で表示させる~なんてのを考えると非同期処理の方が楽かつユーザーにも優しいと思います。

さてロード中のとき、デバッグログに「ロード中だよ」という文字列が断続的に表示されていることに注目してください。
もしもデータのロードが同期的に行われていたなら、この処理は不可能です(画面右下の方でキャラクターがチョコチョコ走る表示を想定)。
コルーチンで非同期的にデータをロードしているから可能なのですね。

並列処理と何が違うのか

非同期処理に似た言葉に並列処理というものがあります。
並列処理と非同期処理は一体なにが違うのでしょうか?
僕が理解した範囲ですと、以下のような感じです。

・非同期処理は関連のない処理を同時に実行する
・必ずしも高速化は意図していない
・並列処理は一つの処理を分割して実行する
・すなわち処理の高速化を意図している

という感じです。
非同期処理の例は先に挙げたのでいいとして、並列処理は例えば以下のようなシチュエーションで使うのではないでしょうか。

・1000という数字を1から100の値で順番に割りたい
・1から50までは並列処理Aで割って、51から100までは並列処理Bで割る

こうすると、最終的な結果が高速に出力されるのではないでしょうか?
ただ実際には並列化を実行するための準備等でコストがかかるため、そのへんもきちんと加味しないといけないようですが……。
あと並列処理した結果の順番を最終的に整形する必要があるのかないのかとかも、速度に関係するようですね。

async/awaitで非同期処理

さて今までコルーチンの非同期処理を見てきましたが、C#での非同期と言えばやはりasync/awaitでしょう。
ですがぶっちゃけ僕はあんまりまだasync/awaitを理解していませんw
これって別スレッドが勝手に生成されるんですかね?
その辺すらよくわかっていないので、もう少し調べときたいと思います。

で、このasync/awaitがUnityで使えないのか? と言えば一応使えるようです。
しかしどうもアセットを入れないと実用的な感じでは使えないっぽいですね。
例えば先ほど使ったUnityWebRequestもawaitできないっぽいです。

とりすーぷさんの記事によれば、最新のUniRxでは「ResourceRequestなどの、AsyncOperationを継承したものをawaitすることができます」とのことなので、「安定なんてくそくらえ! 俺は最新の環境で開発するぜー!」って方はこちらの利用を検討してもよいかもしれませんね。

僕もこちらを使って学習してみたいなーとは思っているのですが、もう少し安定するのを様子見している段階です。

終わりに

非同期処理を理解できればお仕事も増えるだろうということで、そろそろ本格的に勉強しようかなと思っています。
ちなみにUniRx(最新ではないやつ)にはObservableWWW.Getというものがあるようで、これを使うとDLの進捗状況を0.0~1.0の値に正規化した状態で伝えてくれるっぽいです。
つまり進捗プログレスバーの0%から100%表示をやりたいなら、絶対これ使ったほうが楽ですw
実際の開発では僕もUnityWebRequestを直接使うのではなく、ObservableWWW.Getを使いたいと思いますよ。

ほなそんな感じでまた。

フォローする