はじめに
Unityで仕事を初めて2ヶ月が経って、コルーチンを使った非同期処理みたいなのはスムーズに書けるようになってきたが、周りはUniTaskとか使っていてレビューのたびに「え、ここってこういうことなんですか?」みたいなコメントをして人のリソースを無駄遣いしている感じがしているのでかんたんな例を通して改めてUnityの非同期処理についてコルーチンから始めて「UniTask便利ー!」ってなるところまでまとめておきたい。
コルーチンを使う
using System; using System.Collections; using System.Threading; using UnityEngine; public class Coroutine : MonoBehaviour { void Start() { Log("----- 開始 -----"); StartCoroutine( TestCounter(3) ); Log("--- 先に処理 ---"); } IEnumerator TestCounter(int num) { for (int i = 0; i < num; i++) { yield return new WaitForSeconds(1f); Log("COUNT:" + (i + 1)); } Log("----- 終了 -----"); } private void Log(string msg) { int ThreadId = Thread.CurrentThread.ManagedThreadId; Debug.Log("Thread ID=>" + ThreadId + "..." + DateTime.Now.ToString("hh:mm:ss | ") + msg); } }
まずはよく使うコルーチンです。IEnumerator は反復処理をサポートするInterfaceです。処理を複数フレームに分割させて処理させることができるだけであってあくまでシングルスレッドだということが出力結果からわかります。逆を言えばそこまで重くない処理でもyield return
をさせると1フレーム待つことになります(という認識)。あとawait/async他と比べて欠点としてよく挙げられるのが値を返すのが面倒なところです。参照を渡すことなどで実現はできなくはないと思うのですが、僕はまだ見たことがありません。
ちなみにSystem.Threading.Thread.CurrentThread.ManagedThreadId
で現在のスレッドIDを取得できます。
await/asyncを使う
using System; using System.Threading; using System.Threading.Tasks; using UnityEngine; public class Await : MonoBehaviour { void Start() { Log("----- 開始 -----"); Task task = Task.Run(() => AsyncCounter(3)); Log("--- 先に処理 ---"); task.Wait(); Log("--- 後に処理 ---"); } async Task AsyncCounter(int num) { for (int i = 0; i < num; i++) { await Task.Delay(1000); Log("COUNT:" + (i + 1)); } Log("----- 終了 -----"); } }
await/asyncはTaskというクラス(System.Threading.Tasks
)とセットで利用されます。これによってマルチスレッドでの非同期処理が比較的簡単に書けます。(非同期処理≠マルチスレッド。)
ちなみに、task.Wait();
と await task;
は似ていますが、別物です。前者が「スレッドを止めて待つ」のに対して後者は「スレッドを止めずにコールバックを待つ」ものです。 これを理解できていないとデッドロックになりがちです。なった人ー( ´・∀・)ノ ハーィ
await/asyncを使う2
using System; using System.Threading; using System.Threading.Tasks; using UnityEngine; public class Await : MonoBehaviour { private void Start() { _ = TaskAsync(); } async Task TaskAsync() { Log("----- 開始 -----"); Task<int> task = AsyncCounter(3); Log("--- 先に処理 ---"); await task; Log("--- 後に処理 ---"); Log("1 + 1 = " + task.Result); } async Task<int> AsyncCounter(int num) { for (int i = 0; i < num; i++) { await Task.Delay(1000); Log("COUNT:" + (i + 1)); } Log("----- 終了 -----"); return 1+1; } }
今度は処理結果を返り値Task
private void Start() { _ = TaskAsync(); }
としているのには理由があり、詳しくは参考に載せたサイトを見ていただきたいのですが、async void
と書くことで動くには動きます。しかし、非同期で動かすTaskをawaitするか否かはあくまで呼び出し側が決められるようにすべき(ボタンクリックのようなイベントハンドラのような呼び出し側が非同期処理の終了を待つ必要がない場合など例外はある)という理由から推奨されていません。とはいえ、今回はサンプルなので処理の終わりを待つかどうかを利用者に委ねた上で値の破棄をしている、という感じです。
さいごに
まだ少し、Task.Run()とasync/awaitの関係について理解しきれていないところもあるので、次回はそのへんも踏まえつつUniRxやUniTaskについていよいよ触れていきたいと思います。
参考
www.slideshare.net
www.slideshare.net