UnityのMonoアップグレードによるasync/awaitを更にUniRxで対応させる

ついに!.NET 4.6アップグレードが始まりました。Unityの。Unity 5.5でC#コンパイラをアップグレードしていましたが、今回はついにフレームワークも、です。また、Unity 5.5のものはC#のバージョンは4に制限されていましたが、今回はC# 6が使えます。現在はForumでアーリアクセスバージョンが公開されていて、ついでにそこでリンクされているVisual Studio Tools for Unityも入れると、かなりふつーに.NET 4.6, C# 6対応で書ける感じです。

さて、.NET 4.6, C# 6といったら非同期。async/await。もちろん、書けました。が、しかし。

async Task ThraedingError()
{
    Debug.Log($"Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");

    await Task.Delay(TimeSpan.FromMilliseconds(300));

    Debug.Log($"From another thread, can't touch transform position. ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // exception
}

これはtransformのとこで例外でます。なんでかっていうと、awaitによって別スレッドに行ってしまっているから。へー。この辺、async/awaitではSynchronizationContextという仕組みで制御するのですが、現在のUnity標準では特に何もされてないようです。

それだけだとアレなので、そこで出てくるのがUniRx。今日、アセットストアで最新バージョンのVer 5.5.0を公開したのですが、この5.5.0ではasync/await対応を試験的に入れています。それによって自動的にSynchronizationContextも生成/登録してくれます。

async Task UseUniRxInBackground()
{
    Debug.Log($"Start ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(TimeSpan.FromMilliseconds(300));
    Debug.Log($"From same thread, because UniRx installs UniRxSynchronizationContext.ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // show transform
}

というように、UniRxをインポート後では、前の例外を吐いたコードと全く同じでも、ちゃんとメインスレッドに戻してくれるようになります。

Coroutine is awaitable

UniRxを入れることで追加される機能はそれだけではなく、更に普通のコルーチンもawait可能な仕組みを裏側で仕込んでいます。これにより

async Task CoroutineBridge()
{
    Debug.Log("start www await");
    var www = await new WWW("https://unity3d.com");
    Debug.Log(www.text);
    await CustomCoroutine();
    Debug.Log("await after 3 seconds");
}

IEnumerator CustomCoroutine()
{
    Debug.Log("start wait 3 seconds");
    yield return new WaitForSeconds(3);
    Debug.Log("end 3 seconds");
}

といったように、WWWとかIEnumeratorを直接awaitすることが可能になります。これはUniRx側で用意した仕組みによるものなので、普通では(現状は)できません。

勿論(?)IObservableもawait可能になっています。

async Task AwaitObservable()
{
    Debug.Log("start await observable");
    await Observable.NextFrame();  // like yield return null
    await Observable.TimerFrame(5); // await 5 frame
    try
    {
        // ObservableWWW promote exception when await(difference in await WWW)
        var result = await ObservableWWW.Get("https://404.com");
        Debug.Log(result);
    }
    catch (WWWErrorException ex)
    {
        Debug.LogError(ex.ToString());
    }
    Debug.Log("end await observable");
}

ObservableWWWを使うと例外はちゃんとtry-catchのほうに投げてくれるようになって、より自然に、簡単に扱えるようになります。

まとめ

思ったよりも、普通に使えて、普通に統合できるな、という印象があります < async/await。コルーチンで扱うよりも自然で、より強力なので、非同期を扱うのに適したシチュエーションではこっちのほうが良いのは間違いないはずです。Rxとの住み分けですが、基本的に非同期が対象ならばasync/awaitのほうが良いです。が、今回見ていただいたようにIObservableはawaitableなので、コードがRxになっているならば、現在のコードから自然にasync/awaitベースにソフトに移行することが可能でしょう。

Unityが今後、標準でSynchronizationContextを入れてくるのか、コルーチン対応クラスをawait対応にさせてくるのか、などはちょっと分かりません。分かりませんが、UniRxならば、その対応がずっと後のことになるとしても、今すぐ問題なく使うことが出来ますし、その可能性を今すぐ感じ取ることが可能なので、ぜひとも試してみてください!

余談

UniRxがAssetStore STAFFPICKに選ばれましたー。

うーん、嬉しい。

過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの原理原則

という(サブ)タイトルで、.NET Fringe Japan 2016で発表してきました。ニコ生では7時間目ぐらいから。

What, Why, How Create OSS Libraries - 過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの原理原則 from Yoshifumi Kawai

リンク集はこちら。

作り続けることで確実にイディオムが身についていくことと、それの発展や組み合わせによって、より大きなことが出来るようになっていくんじゃないかと思います。発想も、手札が多ければ多いほど、よりよくやれるということが分かるということになりますしね。とはいえ、どうしても発想のベースは自分の手札からになっていくので、時々は異なるものへのチャレンジを意識して行わないとなー、ってとこですね。今回のスライドでも、幾つかはやったことないことを勉強のため、みたいなのがありました。Unity周りは仕事で始めたことですけれど、今は自分の中でも重要な柱です。

C#以外をやりたい、ってのは全然思わないんですが(言語の学習も悪くはないですが、それよりなにか作ったほうが100億倍良いのでは)、今猛烈に足りない/かつやりたい、のはグラフィック関係ですねー。自分で一本メガデモを作れるようになりたいってのは、ずっと昔から思っていることで、かつ、今もできていないことなので近いどこかでチャレンジしたいです。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive