Share
Embed
Transcript
68 slides
-
Reactive Extensions for Unity https://github.com/neuecc/UniRx/ async/await(UniTask) async UniTask<string> DemoAsync() {
// You can await Unity's AsyncObject var asset = await Resources.LoadAsync<TextAsset>("foo"); // .ConfigureAwait accepts progress callback await SceneManager.LoadSceneAsync("scene2").ConfigureAwai // await frame-based operation(you can also await frame c await UniTask.Delay(TimeSpan.FromSeconds(3)); // like 'yield return WaitForEndOfFrame', or Rx's Observe await UniTask.Yield(PlayerLoopTiming.PostLateUpdate); // You can await standard task await Task.Run(() => 100); // get async webrequest async UniTask<string> GetTextAsync(UnityWebRequest req) { var op = await req.SendWebRequest(); return op.downloadHandler.text; } var task1 = GetTextAsync(UnityWebRequest.Get("http://goog var task2 = GetTextAsync(UnityWebRequest.Get("http://bing var task3 = GetTextAsync(UnityWebRequest.Get("http://yaho // concurrent async-wait and get result easily by tuple s var (google, bing, yahoo) = await UniTask.WhenAll(task1, // You can handle timeout easily await GetTextAsync(UnityWebRequest.Get("http://unity.com" -
非同期を同期的に扱う仕組み static string GetSync(int page) { try { var url
= "http://...?page=" + page; var html = GetHttpStringSync(url); return html; } catch { return "Error"; } } static async Task<string> GetAsync(int page) { try { var url = "http://...?page=" + page; var html = await GetHttpStringAsync(url); return html; } catch { return "Error"; } } -
Synchronous Asynchronous Single(1) Multiple(*) var x = f(); var x
= await f(); var query = from person in sequence where person.Age >= 20 select person.Name; foreach (var item in query) { OnNext(item); } var query = from person in sequence where person.Age >= 20 select person.Name; query.Subscribe(item => { OnNext(item); }); IEnumerble<T> IObservable<T> T Task<T> -
Absoutely No C# 5.0(.NET 4.5)でasync/awaitを実装するにあたり、 既にある構造(.NET 4.0 Task)を流用するのが早かった そもそも広く使われるまで分からないこともある 結果、負債もある
midori(MicrosoftのC#風言語によるマネージドOSプロジェクト)での 非同期モデルでの考察においても、Taskに関しては特にパフォーマ ンス面で(OSみたいなシビアなものを作るには)後悔されている http://joeduffyblog.com/2015/11/19/asynchronous-everything/ 現在のC#は、それをValueTaskの独自進化で返済していってる -
Absoutely No C# 5.0(.NET 4.5)でasync/awaitを実装するにあたり、 既にある構造(.NET 4.0 Task)を流用するのが早かった そもそも広く使われるまで分からないこともある 結果、負債もある
midori(MicrosoftのC#風言語によるマネージドOSプロジェクト)での 非同期モデルでの考察においても、Taskに関しては特にパフォーマ ンス面で(OSみたいなシビアなものを作るには)後悔されている http://joeduffyblog.com/2015/11/19/asynchronous-everything/ 現在のC#は、それをValueTaskの独自進化で返済していってる -
async Task<string> SampleTextLoadAsync() { Debug.Log("Before LoadAsync:" +Time.frameCount); // frame:1 var
textAsset = await Resources.LoadAsync<TextAsset>("te") as TextAsset; Debug.Log("After LoadAsync:" +Time.frameCount); // frame:2 return textAsset.text; } -
public class MyAwaiter<T> : INotifyCompletion { bool IsCompleted { get;
} T GetResult(); void OnCompleted(Action continuation); } -
// var result = await foo; は以下のようになる if (awaiter.IsCompleted) {
// もし例外があればGetResultで再throwされる var result = awaiter.GetResult(); // ...awaitの先が実行される } else { // 継続を登録(実際は最適化されてるので毎回ラムダ式は使いません) awaiter.OnCompleted(() => { // もし例外があればGetResultで再throwされる var result = awaiter.GetResult(); // ...awaitの先が実行される }); return; } -
全てのcontinuationを生成しない public async Task FooBarBazAsync() { await Task.Yield(); Console.WriteLine("foo"); await
Task.Yield(); Console.WriteLine(“bar"); await Task.Yield(); Console.WriteLine("baz"); } -
全てのcontinuationを生成しない public async Task FooBarBazAsync() { await Task.Yield(); Console.WriteLine("foo"); await
Task.Yield(); Console.WriteLine(“bar"); await Task.Yield(); Console.WriteLine("baz"); } -
UniRx.Asyncの主要クラス C# 7.0からasyncの戻り値をTask以外で実装できるようになった それを利用した独自のasync対応型で構造体のTask(ValueTask相当) つまりオレオレ非同期フレームワーク C# 7.0はIncremental Compilerか、Unity 2018.3で利用可能 なぜ必要か
全てを差し替えることでTask自体の負債を完全に無視する Unity自体が特殊な実行環境なので特化することで最速を実現する -
Unity is (概ね)シングルスレッド C++のエンジンレイヤー + C#スクリプティングレイヤー C#側での扱いはほとんどシングルスレッド (コルーチン, WWW, AsyncOperation,
etc…) Taskによるasync/awaitは油断するとすぐスレッドプールに飛ばす -> Delay, ContinueWith, Run, etc… async/await(Task)にはマルチスレッド -> シングルスレッドに統合す る機能がついている(SynchronizationContext)、が、そもそもシング ルスレッドなら、その統合レイヤーは消したほうが性能も扱いやす さも上がるのではないか? -
Unity is (概ね)シングルスレッド C++のエンジンレイヤー + C#スクリプティングレイヤー C#側での扱いはほとんどシングルスレッド (コルーチン, WWW, AsyncOperation,
etc…) Taskによるasync/awaitは油断するとすぐスレッドプールに飛ばす -> Delay, ContinueWith, Run, etc… async/await(Task)にはマルチスレッド -> シングルスレッドに統合す る機能がついている(SynchronizationContext)、が、そもそもシング ルスレッドなら、その統合レイヤーは消したほうが性能も扱いやす さも上がるのではないか? -
IEnumerator FooCoroutine(Func<int> resultCallback, Func<Exception> exceptionCallback) { int x = 0;
Exception error = null; yield return Nanikamatu(v => x = v, ex => error = ex); if (error == null) { resultCallback(x); } else { exceptionCallback(error); } } UniTask<int> FooAsync() { var x = await NanikasuruAsync(); return x; } -
IEnumerator FooCoroutine(Func<int> resultCallback, Func<Exception> exceptionCallback) { int x = 0;
Exception error = null; yield return Nanikamatu(v => x = v, ex => error = ex); if (error == null) { resultCallback(x); } else { exceptionCallback(error); } } UniTask<int> FooAsync() { var x = await NanikasuruAsync(); return x; } -
性能のためのUniTask + async/await UniTaskはUnityに特化することでTaskより遥かに性能が良い No ExecutionContext, No SynchronizationContext UniTaskはコルーチン実装よりもアロケーションが少ない 非同期部分ではUniRx(Observable)よりも性能が良い
使い勝手のためのUniTask + async/await シングルスレッド前提のためマルチスレッド的な落とし穴がない 豊富な機能を提供し、コルーチンをほぼ置き換え可能 UniTask Trackerにより、UniTaskのリークを簡単に回避可能 TaskやRxと混ぜて使うことも問題ない -
性能のためのUniTask + async/await UniTaskはUnityに特化することでTaskより遥かに性能が良い No ExecutionContext, No SynchronizationContext UniTaskはコルーチン実装よりもアロケーションが少ない 非同期部分ではUniRx(Observable)よりも性能が良い
使い勝手のためのUniTask + async/await シングルスレッド前提のためマルチスレッド的な落とし穴がない 豊富な機能を提供し、コルーチンをほぼ置き換え可能 UniTask Trackerにより、UniTaskのリークを簡単に回避可能 TaskやRxと混ぜて使うことも問題ない -
public enum AwaiterStatus { /// <summary>The operation has not yet
completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } ValueTaskSourceStatusに合わせています。Taskは本当 に不要なゴミが多くて……) -
public async UniTask<int> FooAsync() { await UniTask.Yield(); return 10; }
public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); return 10; }
public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); return 10; }
public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new System.Exception("Error");
} public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new System.Exception("Error");
} public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new OperationCanceledException();
} public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new OperationCanceledException();
} public async UniTask<int> BarAsync() { var x = await FooAsync(); return x * 2; } public void Baz() { BarAsync().Forget(); } -
public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new OperationCanceledException();
} public async UniTask<int> BarAsync() { var x = await FooAsync(); return x * 2; } public void Baz() { BarAsync().Forget(); } -
public async UniTask<int> BarAsync() { try { var x =
await FooAsync(); return x * 2; } catch (Exception ex) when (!(ex is OperationCanceledException)) { return -1; } } -
public async UniTask<int> BarAsync() { try { var x =
await FooAsync(); return x * 2; } catch (Exception ex) when (!(ex is OperationCanceledException)) { // なんか復旧不能な例外なのでダイアログ出してタイトルに戻る的なことをするとして DialogService.ShowReturnToTitleAsync().Forget(); // fire and forget的に処理 // 元の呼びもとにはキャンセルの連鎖扱いで全てすっ飛ばして終了させる throw new OperationCanceledException(); } } -
キャンセル is 面倒 Rxは戻り値のIDisposableをワンパンすれば良かったのに! (代わりにasync/awaitにはIDisposableというアロケーションはない) かわりに引数に(CancellationTokenを渡して回る) public Task<int> FooAsync(int x,
int y, CancellationToken cancellationToken = default) { var x = await BarAsync(x, y, cancellationToken); return x; } -
キャンセル = OperationCanceledException cancellationToken.IsCancelationRequestedをユーザーコードが チェックする必要はない 何故ならユーザーコード部分は同期だから OperationCanceledExceptionを投げるのは非同期の源流 = asyncOperation.ConfigureAwait(token), UniTask.Delay(token),
etc… とにかく渡す、それだけでいい 非同期の源流が処理してくれるはずなので、そこまで届ければOK まぁそれが面倒くさいんですけどね!!! (性能を落とさず)自動でやってくれる手段はどうハックしてもない -
UnityだとMonoBehaviour/OnDestroyが便利 public class FooBehaviour : MonoBehaviour { CancellationTokenSource cts; void
Start() { cts = new CancellationTokenSource(); } void OnDestroy() { cts.Cancel(); cts.Dispose(); } } -
高コストでは? Yes! キャンセルが例外系であったり、たまにの発火なら、そこまで問題 は出ないはずですが、正常系でのキャンセルが前提になっていると、 状況によりかなり厳しいことが起こりうる 例えばシーンのMonoBehaviourに紐づけて、シーン遷移時に画面に ある10000個のCubeのキャンセルで例外が発火したら… …? UniTask.SuppressCancellationThrow UniTaskではキャンセルを(bool
isCanceled, T value)に変換する SuppressCancellationThrowが用意されている ただし例外抑制ができるのは非同期の源流のみなので注意 -
なんとEventをasync/awaitで実装できる await button.OnClickAsync(); await gameObject.OnCollisionEnterAsync(); というための実装がUniTaskには入ってる async UniTask TripleClick(CancellationToken token)
{ await button.OnClickAsync(token); await button.OnClickAsync(token); await button.OnClickAsync(token); Debug.Log("Three times clicked"); } -
なんとEventをasync/awaitで実装できる await button.OnClickAsync(); await gameObject.OnCollisionEnterAsync(); というための実装がUniTaskには入ってる 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"); } -
パフォーマンスのために ローカル変数では再利用が許されてる(一部の非同期ソースのみ) async UniTask DelayFiveAsync1() { for (int i =
0; i < 5; i++) { // 毎回Delayを生成している await UniTask.Delay(i * 1000); Debug.Log(i); } } async UniTask DelayFiveAsync2() { // Delayを再利用する var delay = UniTask.Delay(i * 1000); for (int i = 0; i < 5; i++) { await delay; Debug.Log(i); } } -
パフォーマンスのために ローカル変数では再利用が許されてる(一部の非同期ソースのみ) async UniTask DelayFiveAsync1() { for (int i =
0; i < 5; i++) { // 毎回Delayを生成している await UniTask.Delay(i * 1000); Debug.Log(i); } } async UniTask DelayFiveAsync2() { // Delayを再利用する var delay = UniTask.Delay(i * 1000); for (int i = 0; i < 5; i++) { await delay; Debug.Log(i); } } -
Don’t be afraid! 性能も問題ない(UniTaskを使えば) プラクティスも既に確立されている(UniTaskを使えば) やりすぎてしまう害もあまりない(強いて言えば非同期汚染) やらない理由がないレベルなので、今すぐGO Recommend to use
with UniRx.Async Unityのために性能/使い勝手ともに再デザインされたasync/await Unityで使うあらゆるものがawaitできるようになる 標準のTaskを使わないことを恐れないで! あらゆる言語、そして通常の.NETも超えた最先端を行きましょう!