はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】Taskでメインスレッドを止める・止めない実例集

はじめに

非同期処理やマルチスレッド処理をするときにTaskを使ったりすると思います。

ただUnityは基本全てメインスレッドで動くので、誤ってメインスレッドを止めてしまった場合他の処理が止まってしまう危険もあります。

f:id:hanaaaaaachiru:20200523160117g:plainf:id:hanaaaaaachiru:20200523160128g:plain
←正しくTaskを使えた場合 メインスレッドが止まった場合→

なぜUnityがメインスレッドで動かす仕様になっているのかと疑問に思うかもしれませんが、以下の公式ドキュメントを一読してみてください。
マルチスレッドとは - Unity マニュアル

今回はメインスレッドを止めてしまう処理と止めない処理を色々と手当たり次第紹介していきたいと思います。

メインスレッドを止める処理(基本NG)

例1

メインスレッドから重い処理を非同期処理にした場合。結局メインスレッド(番号が1)なので意味がない。

// [1] 1 -> [2] 1 -> [3] 1
private async void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    await HeavyMethod();
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private async Task HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例2

重い処理をマルチスレッドにしようとして失敗したパターン。(デッドロックの可能性も)

// [1] 1 -> [2] 179 -> [3] 1
private void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    var task = Task.Run(() => HeavyMethod());
    task.Wait();
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private void HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例3

ちなみに例2でThread.Sleep -> await Task.Delayだったとしても止まる。

// [1] 1 -> [2] 179 -> [3] 1
private void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    var task = Task.Run(() => SomeMethod());
    task.Wait();
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private async Task SomeMethod()
{
    await Task.Delay(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例4

これも重い処理をマルチスレッドにしようとして失敗したパターン。(デッドロックの可能性も)

// [1] 1 -> [2] 275 -> [3] 1, Result : 1
private void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    var task = Task.Run(() => HeavyMethod());
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}, Result : {task.Result}");
}

private async Task<int> HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
    return 1;
}

例5

例4において、Thread.Sleep -> await Task.Delayにしたとしてもメインスレッドは止まる。

// [1] 1 -> [2] 275 -> [3] 1, Result : 1
private void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    var task = Task.Run(() => HeavyMethod());
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}, Result : {task.Result}");
}

private async Task<int> SomeMethod()
{
    await Task.Delay(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
    return 1;
}

メインスレッドを止めない例(こっちを使う)

例1

全部メインスレッドだとしてもawaitができれば止まらない。(重い処理では結局ダメ)

// [1] 1 -> [2] 1 -> [3] 1
private async void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    await SomeMethod();
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private async Task SomeMethod()
{
    await Task.Delay(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例2

重い処理をマルチスレッドで処理するかつメインスレッドに戻ってくるパターン。

// [1] 1 -> [2] 759 -> [3] 1
private async void Test()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    await Task.Run(() => HeavyMethod());
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private void HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例3

例2をもうちょっとカッコよく書くとこんな感じ。

HogeHogeAsyncと分けて書くのがよく見かける王道パターンな気がします。(同期的にもできますしね)

// [1] 1 -> [2] 759 -> [3] 1 , Result : 1
private async void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    var result = await HeavyMethodAsync();
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}, Result : {result}");
}

private async Task<int> HeavyMethodAsync()
{
    var result = await Task.Run(() => HeavyMethod());
    return result;
}

private int HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
    return 1;
}

例4

呼ぶだけ呼んであとは放置(fire and forget)するパターンの非同期処理。

// [1] 1 -> [3] 1 -> [2] 179
private void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    _ = Task.Run(() => HeavyMethod());
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private void HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例5

ConfigureAwait(false)を使うことで、Task.Runの後はメインスレッドに戻らずそのままのスレッドで動かすことができます。

// [1] 1 -> [2] 544 -> [3] 544
private async void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    await Task.Run(() => HeavyMehtod()).ConfigureAwait(false);
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private void HeavyMehtod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例6

UnityAPIはメインスレッドでしか動かないので、例4,例5のときにメインスレッドに戻したい場合はSynchronizationContextを使います。

// [1] 1 -> [3] 1 -> [2] 112 -> [2.5] 1
private void Test()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");
    SynchronizationContext context = SynchronizationContext.Current;
    _ = Task.Run(() =>
    {
        Thread.Sleep(3000);
        Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
        context.Post(_ => {
            gameObject.name = "NewName";
            Debug.Log($"[2.5] {Thread.CurrentThread.ManagedThreadId}");
        }, null);       // UnityAPIはメインスレッドじゃないと動かないのでこれで回避
    });
    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

例7

重い処理を並列で行いたいとき。

// [1] 1 -> [3] 1 -> [2] 10個(全部違うスレッド番号)
private void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");

    // 並列で処理する
    for (int i = 0; i < 10; i++)
        _ = Task.Run(() => HeavyMethod());

    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private void HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

例8

重たい処理を並列で行い、全ての処理が終わったら続きをするパターン。(例7は投げっぱなし)

// [1] 1 -> [2] 10個(全部違うスレッド番号) -> [3] 1
private async void Start()
{
    Debug.Log($"[1] {Thread.CurrentThread.ManagedThreadId}");

    var tasks = new List<Task>();

    // 並列で処理する
    for (int i = 0; i < 10; i++)
    {
        // awaitしてしまったら並列ではなくなってしまう
        var task = Task.Run(() => HeavyMethod());
        tasks.Add(task);
    }

    await Task.WhenAll(tasks);

    Debug.Log($"[3] {Thread.CurrentThread.ManagedThreadId}");
}

private void HeavyMethod()
{
    Thread.Sleep(3000);
    Debug.Log($"[2] {Thread.CurrentThread.ManagedThreadId}");
}

さいごに

色々な使い方をざっと書いてみました。

私自身まだ深く理解していない箇所もたくさんあるので、今後も追記や修正といったこともするかもしれません。

またご指摘やアドバイスといったものは是非コメント等で教えていただけると嬉しいです。

ではまた。