7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unityでメインスレッドに処理を戻して実行する

Last updated at Posted at 2024-02-23

Unity C#で非同期処理にTaskとasync/awaitを使っている人向けです。
UniRxを使わずに、バックグラウンドスレッドからメインスレッドへの切り替えをやりやすく考えてみました。

単にメインスレッドで実行するだけなら

SynchronizationContextを使うことで、メインスレッドに処理を戻すこと自体はとても簡単にできます。

// メインスレッドで実行しておく
var mainContext = SynchronizationContext.Current;

// バックグラウンドスレッドで以下のように実行
// asynchronously
mainContext.Post((_) =>
{
    // ここにメインスレッドで実行したい処理を記述
}, null);

// synchronously
mainContext.Send((_) =>
{
    // ここにメインスレッドで実行したい処理を記述
}, null);

Unityでは自動的にSynchronizationContextのSetが行われているので、メインスレッド上でSynchronizationContext.Currentを参照するだけでメインスレッドのコンテキストを取得することができます。これをあらかじめ取得しておくことで、バックグラウンドスレッドからでもメインスレッドで処理するように指定することができます。
この場合、Postメソッドのcallbackは非同期的に、Sendメソッドのcallbackは同期的に実行されます。

callbackがasyncの場合

上記だけでも簡単にメインスレッドに戻すことができて、これが追加パッケージ無しで使えるというのは素晴らしいことなのですが、SynchronizationContext.Post(Send)メソッドはvoidなのでcallbackからの返り値を受け取ることができません。

メインスレッドでcallbackを実行しつつawaitして完了を待機できるし返り値を受け取ることもできる、ようにしたいですね。

イメージ
async Task SomeBackgroundTask()
{
    // メソッド内は基本的にバックグラウンドスレッドでやりたい処理

    // callbackに渡した処理だけはメインスレッド上で実行したい
    var res = await MainThreadDispatcher.RunAsync(async () => {
        // ここにメインスレッドで実行したい処理
    })

    // メインスレッドでの処理が完了してから実行したい処理
}

そこで、以下のように作ってみました。クラス名はUniRxからパクリ。

MainThreadDispatcher.cs
public static class MainThreadDispatcher
{
    private static SynchronizationContext _mainThreadContext;

    public static void SetMainThreadContext()
    {
        var current = SynchronizationContext.Current;
        _mainThreadContext = current ?? throw new InvalidOperationException();
    }

    // メインスレッドでアクションを実行
    public static void Post(Action action)
    {
        if (_mainThreadContext == null)
            throw new InvalidOperationException();

        _mainThreadContext.Post(_ => action(), null);
    }

    public static Task<TResult> RunAsync<TResult>(Func<Task<TResult>> func)
    {
        var tcs = new TaskCompletionSource<TResult>();
        Post(async () =>
        {
            try
            {
                var res = await func();
                tcs.SetResult(res);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }
        });
        return tcs.Task;
    }
}

以下は動作サンプルです。
Task.Runのcallback内では自動的にバックグラウンドスレッドに切り替わって処理が実行されますが、MainThreadDispatcher.RunAsyncのcallbackではしっかりメインスレッド(ThreadId: 1)に処理を戻すことができています。
また、RunAsyncのcallbackが最終的に返したCompletedをresで受け取ることができていますね。

private void OnEnable()
{
    // メインスレッドで予め呼び出しておく
    MainThreadDispatcher.SetMainThreadContext();
}

void Start()
{
    Task.Run(async () =>
    {
        Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
        var res = await MainThreadDispatcher.RunAsync(async () =>
        {
            // ここに UI スレッドで実行したい処理を記述
            Debug.Log($"This Run on the UI thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(1000);
            return "Completed";
        });

        Debug.Log($"res = {res}");
        Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
    });

}

/** 出力
This Run on the background thread. ThreadId: 52
This Run on the UI thread. ThreadId: 1
~~~ 1秒待機 ~~~
res = Completed
This Run on the background thread. ThreadId: 95
*/

これだけですべてのパターンに対応できるわけではありませんが、メインスレッド上で指定のasyncメソッドを実行し、さらにそれをawaitして結果を受け取ることができるようになりました!

実際のところ

プロジェクトの都合上必要だったので作成してはみたのですが、バックグラウンドスレッドで実行すべき処理だけをTask.Runすれば、上記の動作サンプルではこのクラスは不要ですね。本当に必要な時を見極める必要はあるかもしれません。

こんな感じで
async void Start()
{
    // StartはUIスレッドで実行される

    // awaitでcallbackの完了まで待機可能
    await Task.Run(() =>
    {
        // Task.Runのcallbackはバックグラウンドスレッド
        Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
    });

    // ここはUIスレッドで実行される
    Debug.Log($"This Run on the UI thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1000);
    var res = "Completed";

    await Task.Run(() => {
        Debug.Log($"res = {res}");
        Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
    });
}

以上です。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?