おれんじりりぃぶろぐ

きっと何者にもなれないエンジニアのブログ

UnityにおけるAwaitAsyncを使った非同期実装について②

概略

前回は、最新Unityにおけるマルチスレッドプログラミングの方法についての紹介をしました。 今回はUnityでTask、awaitを使ったマルチスレッドを行う時に起きる問題について取り上げます。

Unityにおけるメインスレッド制約

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task TestAsyncMethod()
    {
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            text.text = "1";  
        });
    }
}

上記のコードを実行すると以下のエラーが出ます。

get_isActiveAndEnabled can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

これはメインスレッド以外からTextにアクセスを行っているため、メインスレッドで実行してくださいというエラー内容になります。 MonoBehaviourを継承しているクラスの使用についてはメインスレッドからアクセスしなければならないという制約があるためです。 では、この問題にはどのような対処方法が考えられるでしょうか?

対処方法

これに対する対処には以下の方法が考えられます。 まずは、stringを返す方法です。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        text.text = await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task<string> TestAsyncMethod()
    {
        var text = string.Empty;
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            text = "1";  
            return text;
        });
        return text;
    }
}

2つ目がコルーチンを使う方法です。 そして、3つ目があるスレッドから特定のスレッド(メインスレッド)にアクセスする方法になります。

あるスレッドからメインスレッドへのアクセス方法

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task TestAsyncMethod()
    {
        var context = SynchronizationContext.Current;

        await Task.Run(() =>
        {
            context.Post((state) =>
            {
                text.text = "1";
            }, null);

            Thread.Sleep(1000);

            context.Post((state) =>
            {
                text.text = "2";
            }, null);

            Thread.Sleep(1000);

            context.Post((state) =>
            {
                text.text = "3";
            }, null);

            Thread.Sleep(1000);
        });
    }
}

SynchronizationContext.Currentは現在の同期コンテキストを取得できるプロパティになり、すなわちメインスレッドを取得することができます。 androidjava層からUnityのメインスレッドのコンテキストを取得して操作を行うイメージに近いと思います。

参考