4
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?

More than 5 years have passed since last update.

Unity非同期処理を理解しようとしてみた

Posted at

Unity非同期処理を理解しようとしてみた

Unityにおける非同期処理を理解するために、Simpleなコードから一歩づつ結果を見ていきます。

環境

  • Windows10
  • Unity 2019.2.11f1
  • Visual Studio 2017

概要

  1. Unityで重い処理を実行してみる
  2. 重い処理を非同期実行してみる
  3. 重い処理の結果を待ってみる
  4. スレッドを追ってみる
  5. 後処理を正しい位置に修正
  6. (参考)Unity用のTaskパッケージ「UniTask」

シーン構成

シーン上にはUpdateMethodで上下に移動するだけのCubeと重い処理が実行されるボタン、そのただ2つだけがあります。

image.png

やりたいこと

  • 重い処理をバックグラウンドで動かしつつ、Cubeの往復を止めない。
  • 重い処理が終わったことを検知して、Cubeを消す。

Unityで重い処理を実行してみる

ここからボタンの処理を書いていきます。
最もシンプルなコードから始めます。
ボタンを押すとHeavyMethodとFinalizeMethodが順番に実行されます。
HeavyMethodは1秒Sleepするだけの単純な処理ですが、このコードを実行するとどうなるでしょうか?

AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method");
        HeavyMethod();

        FinalizeMethod();
        Debug.Log("End Click Method");
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method");
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method");
    }

    void FinalizeMethod()
    {
        Debug.Log("Finalize Method");
    }
}

結果はこちらです。
そしてHeavyMethodの実行中は、Cubeの動きは止まります。
image.png

重い処理を非同期で実行してみる

やっぱり重い処理の実行中は、シーンの処理(この場合はCubeの動き)を止めて欲しくないわけです。
ということで、重い処理を非同期実行にしてみます。

AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method");
        Task.Run(HeavyMethod);

        FinalizeMethod();
        Debug.Log("End Click Method");
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method");
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method");
    }

    void FinalizeMethod()
    {
        Debug.Log("Finalize Method");
    }
}

HeavyMethod();Task.Run(HeavyMethod);に書き換えただけです。
これで結果がこのようになりました。
image.png

先ほどとは順番が変わっていますね。ButtonClickの中には

  1. HeavyMethod
  2. FinilizeMethod

となっているのに、順番が入れ替わって、

  1. FinilizeMethod
  2. HeavyMethod

の順番になっているわけです。

さらに言えば、Heavy Methodが終わる前に、Click Methodを抜けているのがわかります。
この実装であれば、画面のCubeの動きも止まりません。

重い処理の結果を待ってみる

さてここでまた新たな需要です。
「画面の動きは止めたくないけど、Finilize MethodはHeavy Methodが終わってから実行したい。」
わがままですね。
この需要を満たすように、処理を書き換えます。

AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method");
        Task.Run(HeavyMethod);

        // FinalizeMethod();
        Debug.Log("End Click Method");
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method");
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method");

        FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Finalize Method");
    }
}

FinalizeMethodの場所を移動しただけです。
実行結果はこちら。
image.png

この方法もまた、Cubeの移動は止まりません。

でもちょっと待ってください。FinalizeMethodでもし、GameObjectの操作などをしたらどうでしょう?
試してみましょう。

AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method : " + Thread.CurrentThread.ManagedThreadId);
        Task.Run(HeavyMethod);

        // FinalizeMethod();
        Debug.Log("End Click Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method : " + Thread.CurrentThread.ManagedThreadId);

        FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
        Destroy(GameObject.Find("Cube"));
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
    }
}

FinalizeMethodにCubeを破壊する処理と、おもむろにThreadのIdを出力する処理を追加しました。
結果はこちら。
image.png
あれ?
FinalizeMethodが途中で終了していますね。
Cubeも破壊されていないし、例外も宇宙の彼方に消えてしまいました。
Task.Run()で実行したHeavyMethodから以降の処理のThreadのIdが切り替わってますね。非同期にした処理が、マルチスレッドで動いているのがわかります。
Unityのシーン上のオブジェクトは、メインスレッド(番号1のThread)からしか、触れることができないので、このようになってしまいました。

スレッドを追ってみる

  1. Cubeが消えない
  2. 例外が消える

という問題が発生していますが、一旦「Cubeが消えない」にフォーカスして、考えましょう。
DestroyCubeをメインスレッドから実行できればいいわけです。
というわけで修正していきます。

AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    private SynchronizationContext context;
    public void ButtonClick()
    {
        context = SynchronizationContext.Current;

        Debug.Log("Start Click Method : " + Thread.CurrentThread.ManagedThreadId);
        Task.Run(HeavyMethod);

        // FinalizeMethod();
        Debug.Log("End Click Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method : " + Thread.CurrentThread.ManagedThreadId);

        FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);

        context.Post(state => DestroyCube(), null);
        // Destroy(GameObject.Find("Cube"));
        Debug.Log("End Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void DestroyCube()
    {
        Debug.Log("DestroyCube : " + Thread.CurrentThread.ManagedThreadId);
        Destroy(GameObject.Find("Cube"));
    }
}

いきなりcontext = SynchronizationContext.Currentというものが出てきました。
これは、異なるThread間で状態をやりとりするためのオブジェクトです。
このcontextを経由して、FinalizeMethod内でcontext.Post()とすることで、メインスレッドに処理を戻すことができます。
また、本筋とは関係ありませんが、Cubeの破壊とLogの出力をまとめたMethodDestroyCubeを作成しました。

結果はこちら。
image.png
ちゃんとDestoryCubeがメインスレッドで実行され、シーン内のCubeが破壊されました。
ちなみにThreadIdが先程と異なっているのは、Unityが空いてるThreadを適当にPoolからPickUpして使うからです。自分で指定しているわけではありませんよ。

後処理を正しい位置に修正

さて、一応「やりたいこと」は実現できたように思いますね。
ただ、「重い処理の結果を待ってみる」でやったような、FinalizeMethod()HeavyMethod内に書くのはよくない気がします。
HeavyMethodの本来の責務ではないし、構造がネストして複雑になります。
というわけで、FinalizeMethod()を本来あるべきButtonClick内に戻します。

AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    // private SynchronizationContext context;
    async void ButtonClick()
    {
        // context = SynchronizationContext.Current;

        Debug.Log("Start Click Method : " + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(HeavyMethod);

        FinalizeMethod();
        Debug.Log("End Click Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method : " + Thread.CurrentThread.ManagedThreadId);

        // FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);

        // context.Post(state => DestroyCube(), null);
        DestroyCube();
        Debug.Log("End Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void DestroyCube()
    {
        Debug.Log("DestroyCube : " + Thread.CurrentThread.ManagedThreadId);
        Destroy(GameObject.Find("Cube"));
    }
}

ただ戻すだけでは、検証したようにTask.Run()の結果をまたずにFinalizeMethod()が動いてしまいます。
今回はTask.Run()の前にawaitというものがありますね。これは、Task.Run()が完了するまで次の処理を待てよ、という意味です。
これを付けると、VisualStudio2017様に「呼び出し元にasyncをつけろよ」と言われます。言われた通りにasyncをつけました。

結果はこちら。
image.png

いいですね。正しい順番で動いてます。先ほどと違うのは、End Click Methodの位置でしょうか。
Task.Run()で実行した処理の中から、メインスレッドで処理をしたいという需要意外では、SynchronizationContextを使う必要はないってことなんですね。
これならDestroyCubeで発生した例外もcatchできます。

Unity用のTask(UniTask)

今までは使ってきたTaskという仕組みは、C#に備わっている仕組みです。Unityの仕組みではありません。
これはusing System.Threading.Tasks;をimportしていることからもわかりますね。
実は、UnityにはTaskをもっとUnityに特化させたUniTaskというものがあるようです。
これを使うと、処理が速くなるだけではなく後々いろいろメリットがありそうです。
合わせて、今回は自作メソッドごとTask.Run()で呼び出しましたが、今回言及しなかった、別スレッドでの処理の例外をメインスレッドで受けるためにも「UniTaskCompletionSource」などを使って直接awaitできるように修正したら、よりいいのではないでしょうか。

使い方
UniTaskをGitHubから拾ってきて、UnityPackageをimportしたら、using UniRx.Async;を書く。

それでは。

4
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
4
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?