MRが楽しい

MRやVRについて学習したことを書き残す

async/awaitやUniRxを使って非同期処理の後にメインスレッドで処理を行う

本日は UniRx の小ネタ枠です。
async/awaitやUniRxを使って非同期処理の後にメインスレッドで処理を行う手順を記事にします。

非同期処理とUnityのメインスレッド

Unity では非同期処理の後にメインスレッドで処理を行いたいケースが頻出します。
これは Unity のAPIはメインスレッドしか動かないものが多いため、時間のかかる処理を非同期で行った後、Unity のAPIを利用するためにメインスレッドへ戻りたいケースがあるためです。
f:id:bluebirdofoz:20201203231935j:plain

本記事では一例として起動時から10秒のウェイト処理を非同期で行った後、メインスレッドでオブジェクト移動処理を行う処理を実行します。
f:id:bluebirdofoz:20201203231948j:plain

async/awaitでの実行例

以下は、非同期処理の後にメインスレッドで処理を行う処理を async/await を使って記述した例です。
・AsyncAwaitTest.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AsyncAwaitTest : MonoBehaviour
{
    // 起動時処理
    void Start()
    {
        Debug.Log("AsyncAwaitTest Start");

        TestMethod();

        Debug.Log("AsyncAwaitTest End");
    }

    // 定期実行
    void Update()
    {
        // 処理無し
    }

    // テストメソッド
    async void TestMethod()
    {
        Debug.Log("Start ThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);

        // 非同期処理の完了を待機する
        string text = await Working();

        Debug.Log("Result ThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        Debug.Log("Value : " + text);
        // オブジェクトの移動(メインスレッドでのみ動作)
        this.transform.position = new Vector3(0.0f, 1.0f, 0.0f);
    }

    // 非同期処理
    async System.Threading.Tasks.Task<string> Working()
    {
        string text = await System.Threading.Tasks.Task<string>.Run(() => {
            Debug.Log("Working ThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            System.Threading.Thread.Sleep(10000);
            return "Finished";
        });
        return text;
    }
}

オブジェクトにアタッチして実行すると、以下の通り、10秒後にオブジェクトが移動します。
f:id:bluebirdofoz:20201203232041j:plain

ログの ThreadId を確認すると、非同期処理は別スレッドで実行され、完了後の処理はメインスレッドに戻って処理されていることが分かります。
これは await 時に暗黙的に SynchronizationContext の機能が利用され、元のスレッドで処理が継続するためです。
docs.microsoft.com

UniRxでの実行例

以下は、非同期処理の後にメインスレッドで処理を行う処理を UniRx を使って記述した例です。
・UniRxTest.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// UniRxの参照
using UniRx;

public class UniRxTest : MonoBehaviour
{
    private System.IDisposable p_Disposable;

    // 起動時処理
    void Start()
    {
        Debug.Log("UniRxTest Start");

        TestMethod();

        Debug.Log("UniRxTest End");
    }

    // 定期実行
    void Update()
    {
        
    }

    // テストメソッド
    void TestMethod()
    {
        Debug.Log("Start ThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);

        p_Disposable = Observable.Start(() =>
        {
            // 非同期処理
            Debug.Log("Working ThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            System.Threading.Thread.Sleep(10000);
            return "Finished";
        })
        // メインスレッドで完了処理を呼び出し
        .ObserveOnMainThread()
        .Subscribe(text =>
        {
            Debug.Log("Result ThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            Debug.Log("Value : " + text);
            // オブジェクトの移動(メインスレッドでのみ動作)
            this.transform.position = new Vector3(0.0f, 1.0f, 0.0f);
        });
    }

    // 破棄時処理
    private void OnDestroy()
    {
        p_Disposable?.Dispose();
    }
}

オブジェクトにアタッチして実行すると、以下の通り、10秒後にオブジェクトが移動します。
f:id:bluebirdofoz:20201203232053j:plain

ログの ThreadId を確認すると、非同期処理は別スレッドで実行され、完了後の処理はメインスレッドに戻って処理されていることが分かります。
UniRx では Operator を使って実行スレッドを制御することができます。
メインスレッドで処理を行いたい場合は実行例のように ObserveOnMainThread() を指定します。

因みに UniRx のインポート手順は以下を参照ください。
bluebirdofoz.hatenablog.com