Observableで非同期処理
2018/09/15
とりすーぷ
自己紹介
• とりすーぷ(@toRisouP)
• 株式会社バーチャルキャスト
• Unityクライアント開発
• 人類を美少女にする仕事をしてます
非同期について
非同期処理って何?
非同期処理って何?
• 関数呼び出し時に、結果を待たずに次に進む処理のこと
• 処理内容を別のタイミングで実行する処理のこと
非同期処理≠マルチスレッド処理
• 非同期だからといってマルチスレッド処理とは限らない
• シングルスレッドでも非同期処理はできる
• マルチスレッドでも同期処理で書くことはできる
• 一緒くたにされることがよくあるが、厳密にいうと違う概念
非同期処理の難しさ
非同期処理は難しい?
• 非同期処理は一般的に「難しい」と言われる
• 何が難しいの?
非同期処理の難しいところ
• 実行結果の取り扱い
• 直列/並行処理
• エラーハンドリング
• 実行キャンセル
• 実行コンテキストの制御(マルチスレッド時の制御)
非同期処理 vs 人類
• 非同期処理へ無防備に立ち向かうと人は死ぬ
• 非同期処理は愚直に書くとすぐ手に負えなくなる
• いろいろな対策手法が考案されてきた
非同期処理の手法
Unityで今の所使えるもの
• Unityコルーチン
• async/await + Task
• UniRx ( Observable )
• UniRx ( UniRx.Async )
• 独自実装
Unityコルーチン
• Unityメインスレッド上で処理を待ち受ける機構
• 結果を返すことを考慮していない
• GameObjectと処理がひもづく
async/await + Task
• C#の言語機能として用意されている機構
• Unityの場合は.NET 4.xモードにしたら使える
• サクッと非同期処理が書けて非常に使いやすい
UniRx ( Observable )
• UniRxのメイン機能
• Taskの拡張版みたいなもの
• 同梱されているOperatorやSchedulerが便利
UniRx ( UniRx.Async )
• ascyn/awaitとTaskをUnity向けに最適化したもの
• 詳しくはneueccさんの講演で
独自実装
• 非同期処理の機構を自前で作ってしまう方法
• 単純なものならわりとすぐ作れる
• 実行コンテキスト制御や、キャンセル周りを考えるとツライ
結局どれを使えば?
• まず第一にasync/awaitを使おう
• async/awaitで表現できないものはObservableでやる
以前、話しました
• Task vs Observable
• https://niconare.nicovideo.jp/watch/kn3081
ここまでが前置き
本題
今回話したい内容
• UniRx(Observable)の非同期処理への活用方法
• UniRxは「イベント処理のライブラリ」として浸透しちゃった
• UniRxは非同期処理にも使えるよ、という話がしたい
Observable
Observable
• Reactive Extensionsに登場するメッセージ処理のオブジェクト
• 「ストリーム」とか呼んでたりするアレの正式名称
• Message Source + Operator + Scheduler で構成される
Unity開発で多用される
• ゲームはイベントが飛び交う
• イベント処理に強いObservableはとても便利に使える
イベント処理と非同期処理
イベント処理
• 何かの条件を満たしたときにメッセージを発行する
• メッセージを受け取った側が、何かの処理を実行する
• 発行されるメッセージ数は不定
(0個だったり、有限個だったり、無限だったり)
非同期処理
• 処理が終わったときに、メッセージを発行する
• メッセージを受け取った側が、何かの処理を実行する
• 発行されるメッセージ数は必ず1個
(成功 or 失敗 で必ずどちらかの結果を1つ返す)
Observableから見ると
• イベント処理
• OnNextを複数個発行する(0~N個、無限もあり)
• 非同期処理
• OnNext必ず1つ発行する(成功時)
• 失敗時はOnErrorを返す
どっちもObservableの表現範囲内
• 非同期処理もイベント処理も等価に扱える
• メッセージの発行のパターンが異なるだけ
• 「イベント処理」のメッセージ長を1に固定したら「非同期処理」
Observableのメリット
コルーチンと比較してみよう
コルーチンと比較
• コルーチンは結構プリミティブに近い作り
• 非同期処理の機構だけど、それほど機能は多くない
• 実行結果を返せない、エラーハンドリングしてくれない、等
• Observableで書くと非同期処理がどれくらいキレイになるのか
例:HTTP GET
• WWWでGETする処理を比較
• コルーチンで書いた版
• コルーチンをObservableに変換した版
コルーチン版
• HTTP GETする
• 結果をログに出すだけ
Observable版
• Observable.FromCoroutine
• コルーチンをObservableに変換
Observable化
• 中身はコルーチンだけど、ガワはObservableになった
• 「Observableで包んだ」というのが大事
• これだけで十分使えるようになる
比較
• 実行結果の取り扱い方
• 直列/並行処理
比較
• 実行結果の取り扱い方
• 直列/並行処理
コルーチン
• コルーチンに結果を返す機能はない
• デリゲートを使ったコールバックで無理やり返すしかない
• 結果をキャッシュしておいて後で使う、がやりにくい
コルーチンで実行結果をむりやり返す
Observableの場合
• 結果を「IObservable<T>」で扱うことができる
• 変数に代入して取り回せる
• 結果をキャッシュしておいて後で取り出せる( 要PublishLast)
Observableで結果を扱う
比較
• 実行結果の取り扱い方
• 直列/並行処理
直列・並行処理
• 直列:前の処理の終了待って、次の処理を実行する
• 並行:複数の非同期処理を同時に実行して終わるまで待つ
直列処理
• コールバック手法と相性がよくない
• ネスト地獄になる
• 途中で処理分岐が入るとぐちゃぐちゃになる
コールバック手法で直列処理
• 比較がしたいので、コールバックで結果を返す方法で説明する
コールバックを使って直列処理(2段)
1回目のGETの結果を使って、
2回目のGETを実行する
直列3段
1回目のGETの結果を使って、
2回目のGETを実行し、
その結果を使って3回目のGETを実行
コールバックではすぐ破綻が見える
• 正常系の処理だけでもすでに読みにくい
• エラーハンドリングが入るとさらに処理が分岐する
• 途中で例外飛んだらどうするの?
Observableで直列処理を書く
• ContinueWith (またはSelectMany)で連結するだけ
• フラットな形式で書ける
並行処理
• Unityコルーチンでは並行で結果を待ち受ける機能はない
• 実行はできるけど、結果を待てない
• 自作する必要がある
• めんどい
Observableで並行処理
• Observable.WhenAll
• これで包むだけ
直列・並行処理
• Observable化したほうが基本的には楽になる
• ネスト地獄を回避できる
コルーチンとの比較 まとめ
• Observableを使えば楽になる部分が多い
• コルーチンと比べて結果の取り扱いが非常に楽になる
• 処理の連結も楽にできる
ちなみにコルーチンに限った話ではない
• 非同期処理をObservableにすればこのメリットは享受できる
• 例:独自機構の非同期処理をObservableにラップする
覚えておきたいUniRxの機能
UniRxには機能がたくさん
• Rx.NETからある機能から、Unity特化の機能まで
• その中でも非同期処理に使えるものを紹介
紹介する機能
• ContinueWith
• エラーハンドリング
• Observable.Start()
• Observable.Create()
• AsyncSubject
• PublishLast()
• Observable.FromCoroutine()
• ToYieldInstruction()
ContinueWith
ContinueWith
• Observableを直列に連結するときにつかうOperator
• 引数は Func<T, IObservable<TResult>>
• SelectManyの軽量化版(OnNext 1回限定で動作)
エラーハンドリング
失敗したときどうするの?
• 失敗時のリカバリをキレイに書ける?
• ログに出して終わり
• リトライする
• 別の処理にフォールバックする
• 処理を諦める
• 失敗を握りつぶす
Operatorでエラーハンドリング
• ObservableはOperatorでエラーハンドリングできる
• すごいいっぱい機能がある
• async/awaitには無い、Observableの強み
Catch
• OnErrorを受け取って、別のObservableに差し替える
• 別の処理にフォールバックさせる
CatchIgnore
• OnErrorを受け取って、Observable.Emptyに差し替える
• 別のObservableにフォールバックさせる必要が無いときに使う
例外の型でフィルタリング
• デリゲートの型を明示的に指定するとフィルタリングできる
Retry
• OnErrorがきたら、Subscribeからやり直す
• Retry()より上の層を再生成して実行しなおす
• 回数指定ができる
OnErrorRetry
• Retryの強化版
• Catch + Retry
• 挙動をかなり細かく制御できる
Timeout
• タイムアウトさせる
• 指定した時間が経過、または指定時刻になったらOnErrorを発行
エラーハンドリングまとめ
• Operatorがかなり優秀
• 組み合わせるだけでそれなりのエラーハンドリングができる
• Retryは使用箇所を見極めよう
• リトライすれば問題が改善するかもしれない場面でのみ使おう
• 良:ネットワーク通信(リトライで改善するかもしれない)
• 悪:ローカルファイルの読み込み(ファイルが無い場合、リトライしても無い)
AsyncSubject
AsyncSubject
• 非同期処理向けSubject
• メッセージを1個だけキャッシュする機能を持つ
• OnCompletedすることで状態が確定する
普通のSubject(AsyncSubjectではないやつ)
• Subject
• メッセージ発行の根源になるオブジェクト
• IObservable<T>とIObserver<T>を両方とも実装している
(普通の)Subjectの挙動
遅れてSubsribeした場合、それより前に
発行されたメッセージは受け取ることができない
AsyncSubjectの挙動
AsyncSubjectは、OnCompleted済みであれば後から
Subscribeしてもメッセージを受け取ることができる
AsyncSubjectの使い方
用途
• 非同期処理の結果通知にそのまま使える
• JavaScriptのPromiseみたいな使い方をする
• 処理の結果をキャッシュして保持させる
• 1回実行した非同期処理の結果を使いませる
応用例:コンポーネントの初期化順序
• コンポーネントの初期化順序が問題になる場面
• シーン開始直後などの初期化フェーズ
• Aが先に実行されてないと、Bの処理で失敗する
• 苦肉の策でScript Execution Orderでゴリ押し解決するの…?
例
• PlayerProvider : Playerを生成して提供する
• PlayerInitializer : 生成されたPlayerの初期設定を実行する
例:PlayerProvider
例:PlayerInitializer
例:コンポーネントの処理順序
• この2つのコンポーネントは実行順序が重要
• PlayerProvider -> PlayerInitializer
• 順序が崩れるとエラーが出て動かない
非同期処理にして解決する
• 「初期化は非同期処理が連鎖する」という考え方に切り替える
• 非同期的にPlayerが初期化される、と考える
• PlayerInitializerはその非同期処理を待つ
例:非同期化
例:非同期化
AsyncSubjectの性質により
Subscribe()を先に実行しようが後から実行しようが、
OnCompletedしたときに必ずPlayerが取得できる
実行順序を気にしなくてよくなった
• 必ずPlayerProvider -> PlayerInitializerの順番で実行される
• AsyncSubjectを使えば非同期処理に変換できる
• もしPlayerProviderの処理が後から
本当に非同期処理になっても、PlayerInitializer側は変更不要
ちなみに:await
• AsyncSubjectはAwaitable
• async/awaitで待機できる
• IObservableのGetAwaiter()は、内部でAsyncSubjectに変換している
async/awaitで待つ
• PlayerInitializerの初期化をasync/await化
• 同期で書いたときと同じ記法にできる
AsyncSubject まとめ
• 非同期処理を扱うためのSubject
• そのまま非同期処理の結果通知に使える
• コンポーネントの順序関係の整理にも使えそう
• 後から本当の非同期処理が混ざってきても対応できる
• async/awaitが使えるなら記法もほとんど変えずに済む
PublishLast()
PublishLast()
• Hot変換Operator
• AsyncSubjectを使ったHot変換を行う
Hot変換?
Observableの性質
• 基本的に、Subscribe()されるまで動かない
• Subscribe()される前の状態は「仮止め」みたいな状態で放置される
• Subscribe()されると、はじめてインスタンス化されて動く
• こういう状態のObservableを「Cold Observable」と呼ぶ
例:Observable.Interval
例:Observable.Interval
定義したタイミングでは、Observable.Intervalは起動しない
例:Observable.Interval
Subscribeされたタイミングで起動する
例:Observable.Interval
Subscribeされたタイミングで起動する
3回Subscribeされたので、
3つタイマーが起動する
Cold Observable
• 仮止め状態で浮いているObservableのこと
• Subscribeされて初めてインスタンス化して起動する
• Subscribeされた回数分インスタンス化される
Hot Observable
• 既に稼働済みのObservable
実はLINQも似ている
• Cold Observableみたいな現象はLINQでも起きる
LINQ
• LINQのメソッドはforeachで呼ぶまで評価されない
定義したときにはまだ評価されない
← foreachを回したときに評価される
LINQと多重foreach
• こういう書き方をするとちゃんと動かない
LINQと多重foreach
• こういう書き方をするとちゃんと動かない
← ここ
← ここ 1回目のforeachと、2度目のforeachとで、
別々のWebSocketClientが生成されてしまう
LINQにもColdっぽい性質がある
• foreachされるまで評価されない性質
• 正確に言うとMoveNext()されるまで評価されない
• じゃあ解決方法は?
この問題の解決方法
• 先にLINQを一回実行し、結果をListやArrayに詰め直す
この問題の解決方法
• 先にLINQを一回実行し、結果をListやArrayに詰め直す
以降の処理は、結果の方を使う
省略記法
• 面倒くさいので普通はToList()とかToArray()を使う
LINQとRxはよく似ている
• ObservableとEnumerableが双対の関係
• 双対の概念だけあって、結構似ている
• LINQのときの対処方も、Observableへ適用できるのでは?
Cold to Hot
• Cold ObservableをHotにするには?
Cold to Hot
• Cold ObservableをHotにするには?
• LINQでいう「foreachを事前に実行する」的なことをすればよい
Cold to Hot
• Cold ObservableをHotにするには?
• LINQでいう「foreachを事前に実行する」的なことをすればよい
• Subescirbe()を事前に実行すればよいじゃん!
Hot変換のやりかた
• Subjectを使って事前にObseravbleをSubscribe()をしてしまう
• LINQを1回実行してArrayに詰め直す、に似てる
例:Subjectを使ってHot変換
• Subjectを使って、対象を先にSubscribe()する
• 以降のObserverはSubjectの方を代わりにSubscribeする
例:Subjectを使ってHot変換
• Subjectを使って、対象を先にSubscribe()する
• 以降のObserverはSubjectの方を代わりにSubscribeする
← ここのSubscribe()でObservable.Intervalが起動する
例:Subjectを使ってHot変換
• Subjectを使って、対象を先にSubscribe()する
• 以降のObserverはSubjectの方を代わりにSubscribeする
Hot変換後にメッセージがほしいなら、
Subjectの方をSubscribe()する
AsyncSubjectでもHot変換できる
• Subjectの代わりにAsyncSubjectも使える
• 結果をキャッシュするという性質がObservableに付与できる
• 非同期処理を扱うObservableと相性がとても良い
AsyncSubjectでHot変換
• 先に処理を実行しつつ、その結果をキャッシュできる
AsyncSubjectでHot変換
• 先に処理を実行しつつ、その結果をキャッシュできる
関数を呼んだ時点でObservable.FromCoroutineが実行
(コルーチンが稼働しはじめる)
AsyncSubjectでHot変換
• 先に処理を実行しつつ、その結果をキャッシュできる
何回Subscribeしてもキャッシュを参照する
(コルーチンの多重起動を防止できる)
Hot変換 まとめ
• Cold Observableという性質を覚えよう
• Subjectを使えばHotに変換できる
• AsyncSubjectでHot変換すると非同期処理との相性がよくなる
PublishLast()
の話にもどる
PublishLast()
• Hot変換を実行するためのOperator
• 中でAsyncSubjectを作ってSubscribeしてくれる
仲間たち(中のSubjectが異なる)
• Publish()
• Subject
• Publish(T initValue)
• BehaviorSubject
• PublishLast()
• AsyncSubject
• Replay()
• ReplaySubject
PublishLast()の使い方
• Observableの末尾につけて、その後にConnect()を呼ぶ
• 返り値はIConnectableObservable
• 中でAsyncSubjectを作って、Subscribeしてくれる
• ただしIConnectableObservableのリークには注意
PublishLast()を使うメリット
• Observableを先走りさせることができる
• 非同期処理を先に実行しておける
• 結果をキャッシュできる
• 結果を後から好きなタイミングで取り出せる
• 同じ処理を重複して実行させなくてすむ
ただし…
• PublishLast()にはデメリットもある
PublishLast()を使うデメリット
• 「失敗」もキャッシュしてしまう
• キャッシュされた結果に対して、Retryはしても無意味
• エラーハンドリング不可能になってしまう
PublishLast()とエラーハンドリング
PublishLast()とエラーハンドリング
ここでOnErrorが発生すると、
その状態もキャッシュしてしまう
PublishLast()とエラーハンドリング
ここのRetryはOnErrorが確定したAsyncSubjectに対して
実行されるため、Retryする意味がない
PublishLast()の使いどころ
• 失敗がありうる場所では使わない方がいいかも
• PublishLast()する前にエラーハンドリングを書く
• PublishLast()を使わずにColdのままにしておく
• このあたりはケース・バイ・ケース
PublishLast() まとめ
• PublishLast()でお手軽にHot変換ができる
• 非同期処理との相性がよい
• ただし、デメリットもある点に注意
拡張メソッド作っておくと便利
• PublishLast().Connect()までセットで実行する
• RefCount()とPublishLast()は相性が悪いので使えない
Observable.Start()
Observable.Start()
• Task.Run()とほぼ同じ機能
• Func<TResult>を実行し、結果をOnNext()として出力する
• Schedulerの指定も可能
使用例
• 同期処理をかんたん非同期化
• マルチスレッドを使った非同期処理に変換できる
Observable.Start()はCold
• Observable.Start()は、Subscribe()しないと起動しない
• 先に処理を走らせておいて、
後から結果を取り出したいときは、PublishLast()を使おう
• Observable.ToAsync()…?知らない子ですね…。
Observable.Start() まとめ
• 同期処理を非同期処理化するのに便利
• サクッとマルチスレッド処理化できてよい
Observable.Create()
Observable.Create()
• 任意の手続き処理から、任意のObservableを作る機能
• 自由にObservableがつくれる
使い方
使い方
指定した型のIObserver<T>が引数で渡される
使い方
IObserver<T>を使って自由にメッセージ発行
使い方
終了時の後片付けに用いるDisposableを返す
用途は無限にある
• なんでもできる
• 中でスレッド起動してマルチスレッド処理
• 既存の非同期処理を隠蔽
例:既存の非同期処理のラップ
• 既存の非同期処理の機構をObservableに変換できる
• 例:使っているライブラリをObservableに変換して使いたい
ニフクラSDKの通信API
• デリゲートによるコールバック呼び出し方式
ニフクラSDKをObservable化
ニフクラSDKをObservable化
SubscribeOn()
• Subscribe()を実行するコンテキストを切り替える機能
• 指定したScheduler上でSubscribe()を実行する
• 例:ThreadPool上にスイッチしてからSubscribe()
• 例:Unityメインスレッドに戻ってきてからSubscribe()
SubscribeOn()とObservable.Create()
• Observable.Create()はSubscribe()されたコンテキストで動く
• SubscribeOn()を併用すると、
実行コンテキストを切り替えできる
同期的に書いたObservable.Create()
• 同期的に書いてある
• このまま実行するとスレッドをブロックして処理する
そのまま使う
• Unityメインスレッドをブロッキングして読み込む
• ブロッキングしても問題ないならこれで良い
SubscribeOn()と組み合わせる
• SubscribeOn()でThreadPoolを指定
• 処理をThreadPoolに移動できる
• ObserveOnMainThread()で結果メインスレッドに戻す
Observable.Create() + SubscribeOn()
• Observable.Createの実行コンテキストを切り替えられる
• 呼び出し側でマルチスレッド処理に変更できる
Observable.Create まとめ
• なんでもできる
• 既存の非同期処理をObservableに変換したりできる
• SubscribeOnと組み合わせるとより便利
• とりあえず中身を同期で書いておいて、
実行時にマルチスレッドで非同期化できる
Observable.CreateWithState
• ラムダ式の内部に変数を閉じ込めるときに使う
• ラムダ式に外部変数をキャプチャさせたくないときに使う
Observable.FromCoroutine
Observable.FromCoroutine
• コルーチンからObservableを作る機能
• 種類がいくつかある
種類
• Observable.FromCoroutine / FromMicroCoroutine
• コルーチンが終了するのを待てる
• Observable.FromCoroutineValue
• コルーチンのyield return した値を取り出せる
• Observable.FromCoroutine<T> / FromMicroCoroutine<T>
• コルーチンから任意のメッセージを発行できる
Observable.FromCoroutine<T>
• コルーチンから任意のObservableを作る機能
• Observable.Createのコルーチン版みたいなもの
例:コルーチンからObservableへ
例:コルーチンからObservableへ
使いみち
• コルーチンから結果を取り出すことができるようになる
• コルーチン最大の欠点を解消できる
• 手続き処理からObservableを作れる
• Operatorだけでは表現しにくい処理がコルーチンを使うと
比較的かんたんに書けたりすることもある
Observable.FromCoroutineはCold
• 必要に応じてHot変換するとよい
Observable.FromCoroutineまとめ
• コルーチンからObservableへ変換できる
• コルーチンから結果を取り出すのが容易になる
• 手続き処理ベースでObservableを作れる
ToYieldInstruction()
ToYieldInstruction()
• ObservableをIEnumeratorに変換する機能
• コルーチン上でObservableの完了待ちができるようになる
使い方
使い方
ObservableYieldInstructionが返される
(IEnumerataorを実装したオブジェクト)
使い方
Yield return でObservableの完了(OnCompleted)を待てる
結果をそのまま取り出せる
使いみち
• async/awaitっぽいことがコルーチンで実現できる
• 非同期処理を同期っぽく書くことができる
• async/awaitと比べると冗長ではある
合わせ技
• Observable.FromCoroutine & ToYieldInstruction
• かなりの表現力が得られる
• async/awaitがないUnityバージョンでも十分戦えるだけの力はある
合わせ技
合わせ技
合わせ技
キャンセルの仕方
• ToYieldInstructionはUniRx管理のコルーチン上で動く
• 元のコルーチンを止めてもToYieldInstructionの
待受はキャンセルされない
止まらない例
止まらない例
GameObjectを破棄すると、
こっちのコルーチンは止まる
止まらない例
ToYieldInstaruction()は止まらず、
裏でずっとObservableをSubscribe()したままになる
対策
• Observable.FromCoroutine経由でコルーチンを起動する
• そのときにCancellationTokenをToYieldInstruction()に渡す
CancellationTokenを使って止める
ToYieldInstruction() まとめ
• コルーチン上でObservableを待機できる
• OnCompleted()がくるまで待ち受ける
• 非同期処理の待機に使える
• async/awaitっぽくObservableが待てる
• Observable.FromCoroutine()とあわせるとさらにつよい
まとめ
非同期処理
• 結果をすぐに返さない処理
• 結果を待たずに関数呼び出しが終了する処理
• マルチスレッド処理とは別の概念
非同期処理の手法
• 非同期処理はやり方がいくつかある
• async/await + Task
• Unityコルーチン
• Observable
• UniRx.Async
結局何を使えばいいの?
1. async/awaitを最優先に使う
2. async/awaitが使えない環境の場合、
またはasync/awaitでは表現力が足りない場合、
Observable(UniRx)を使う
3. Observableも使えない場合は独自機構を作ってがんばる
Observableは非同期処理に使える
• イベント処理だけでなく、非同期処理にも応用ができる
• Observableには非同期処理で使うと便利な機能がたくさんある
• ただしHot/Coldの性質だけは注意
本当にまとめ
まとめ
• 非同期処理とは何なのかを覚えよう
• 結果を待たない処理
• いろんな手法でコストを下げることができる
• Observableは非同期処理に使える
• UniRxはイベント処理以外にも使える
• Hot/Coldの性質だけは本当に注意

Observableで非同期処理