LoginSignup
2

More than 3 years have passed since last update.

Unityでのasync・await・lock

Last updated at Posted at 2019-11-22

はじめに

Unityにおける非同期自体は需要が高いこともあり、いい記事もたくさんあります。
しかし、コルーチンより Task (async・await)を使うメリットが書いてある記事や lock(非同期時における変数へのアクセス制御)についても書いてある記事が少ないのでそこらへんについて書いていきます。
なお、実装に関してはほぼ書きません。リンク先を参考にしてください。(というかみんな良記事を書くから私が書く理由を見出せなかった。)
非同期について深くやった理由は↓のやつを作ったからです。
FaceRigなんてもう古い!Vをやるならこれを使え!

環境

  • Unity 2018.3.8f1

非同期の必要性

そもそもなんで非同期が必要か。非同期というのはかなり難しいとこです。(なのでみんな悩まされるのですが。)その理由は、デメリットをはるかに超えるメリットがあるからです。
主なメリットは二点。

  • メインの処理を止めずに時間を待てる
  • 処理の高速化ができる

このうち、上のものは Unity がコルーチンという形で簡単に扱えるようになっています。Task (async・await)は両方できます。

非同期に関する良記事まとめ

先に非同期について非常に良い記事のリンクを置いておきます。実装するときはこちらを参考にしてください。なお、私とは一切関係ありません。

async・await をやる理由

正直、コルーチンより Task (async・await)のほうがはるかに難しいです。それでも Task (async・await)を使う理由は、パフォーマンスのためです。Task (async・await)はマシンのスペックをフルに使えるのです。
CPUって、〇コア・〇スレッドとありますよね。コルーチンは1コア・1スレッドしか使えず、 Task (async・await)は(調整すれば)すべて使えます。
なぜそうなるのかをちょっとイラスト化してみました。
舞台はUnity社。現実の Unity Technologies Corporation は全く関係ありません。名前の都合です。

コルーチンの場合

どうやらお仕事のようです。
corutine.png

なお、業務は実際は関数となります。

作業が終わるとこうなります。
corutine2.png

しかし...実際に働いている人は一人しかいないので問題も起こります。Unity社ではまだ人は余っているのに。
corutine3.png

こうなると次のシーンは予想できますよね。
corutine4.png

これは、私たちにおけるフレーム落ちや、重いといった結果になります。つまり、私たちはきちんと時間と計算量を考慮に入れてコルーチンを作る必要があります。

Task (asinc・await)の場合

こんどはちょっと違います。
task1.png
task2.png

こんな風に雇っていたバイト君を使い倒します。

なので責任者も安心です。
task3.png

Task (asinc・await)のはまりポイント

このように大きなメリットのある Task (asinc・await)ですが、気をつけなければいけないところもあります。

Unityのオブジェクトを操作するとき

Unity社の資産は大事です。なくしたり、壊されたりするのは困ります。なので、Unity社ではバイト君たちにはいじらせるということは許可されてません。もしバイト君たちがいじろうとしたらxxx can only be called from the main thread.というエラーが吐かれます。
ではどうするか。答えは簡単で、責任者に任せます。

public class Temp : MonoBehaviour
{
    private SynchronizationContext _context;    

    private void Start()
    {   // 現在のスレッド情報を取得。MonoBehaviourを継承したクラスはメインスレッドを取得。(責任者を覚える)
        context = SynchronizationContext.Current;
        Task.Run(MoveAsync);
    }
    //非同期にこのオブジェクトを移動させるメゾット。返り値voidはよくないけど無視。
    private async void MoveAsync()
    {
        while(true){
            //_contextに保存したスレッドでラムダ式の中身を実行させる。第二引数のnull忘れずに。
            //要は責任者に実行だけさせる。
            _context.Post(() =>{
                var pos = transform.position;
                pos.x += 1;
                transform.position = pos;
            }, null);
            await Task.Delay(100);
        }
    }
}

このように、エラーで怒られるところをメインスレッドで実行させるようにします。

同じオブジェクトを操作するとき

以下の例を考えましょう。なお、変数aは数字です。

バイトA君は

  • 変数aを読み込む
  • aに1加える
  • aに反映させる

バイトB君は

  • 変数aを読み込む
  • aから1引く
  • aに反映させる

とします。A君とB君が同時に操作させます。結果は元と変わらないはずです。しかし、もしこれが以下の順番で起きたとします。

  1. A君読み込み
  2. B君読み込み
  3. A君計算
  4. B君計算
  5. A君反映
  6. B君反映

そうすると、B君の変更のみ適応された、つまり、元の数から1引いた数が結果となり、バグとなります。こういうことが起こるのは稀と思う人もいるかもしれませんが、マルチスレッドをやると無視できないぐらいには起こります。
これを防ぐにはlock構文を使います。

lock構文

lock構文の基本は以下のコードです。

    lock(someobject){
    // ここに同時にされてはまずい処理を書く。
    }

lock構文では、someobjectにあたる部分にobjectを与えることにより、中の処理は同じsomeobjectのlock構文では必ず1か所のみでしか実行されません。他のところでこのlockに行きつくと、そのコードは待機します。前のlockがブロックを出たら、lockに入ります。

実際に例を見ましょう。

public class Temp : MonoBehaviour
{
    private int count = 0; 
    private int count2 = 0; 
    private object lock_count = default;          // どちらも
    private object lock_count2 = new object();    // 同じ

    private void Start()
    {   
        Task.Run(Incriment1);
        Task.Run(decriment1);
        Task.Run(Incriment2);
        Task.Run(decriment2);
    }

    private void Incriment1()
    {
        while(true){
            lock(lock_count){
                count++;
            }
        }
    }

    private void Decriment1()
    {
        while(true){
            lock(lock_count){
                count--;
            }
        }
    }

    private void Incriment2()
    {
        while(true){
            lock(lock_count2){
                count2++;
            }
        }
    }

    private void Decriment2()
    {
        while(true){
            lock(lock_count2){
                count2--;
            }
        }
    }
}

このようにすることで矛盾なくできるようになります。このコードは一見lock要らなさそうに見えますが、必要です。(といってもこのコードもうまく動くとは思わないけど。)

lockに関する良記事

詳しくlockやそれに似たものを知りたい場合は以下の記事が参考になると思います。(例えば今回の例だとさらに早くなります。)

さいごに

非同期は難しいけど使いこなすと非常に強力です。みなさんがんばってください。

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
2