Unityでasync/await、Taskを実用レベルでしっかり理解するために必要な情報をまとめました。
- はじめに - async?await?Taskとは?
- MonoBehaviourのメソッド(Startとか)を非同期メソッドにする
- 結果を返すTaskを作る
- 複数のTaskを直列で実行する
- 複数のTaskを並列で実行する
- Taskをキャンセルする
- 例外処理について
- タイムアウト処理
- await後に元のスレッドに戻さない
- 参考
Unity2018.2
C#6
はじめに - async?await?Taskとは?
async/await、Taskを学ぶ上で最初にするべきことは非同期処理の概念を理解することです。
次の記事では非同期処理の概念とasync/await、Taskの基本的な使い方をわかりやすく説明しています。
必要に応じて参照してください。
MonoBehaviourのメソッド(Startとか)を非同期メソッドにする
前節で紹介した記事の最後で、MonoBehaviourのStart()
から非同期メソッドを呼ぶときには
var _ = ExampleAsync();
のように書くという話をしました。
それでも問題ないのですが、そもそもこのStart()
メソッドを非同期メソッドにしてしまう、という手もありそうです。
private async Task Start()
{
await ExampleAsync();
}
戻り値はTaskにしてますが、これらは基本的にUnityからしか呼ばれないメソッドになるので、
例外的なケースとしてasync voidでも問題はないんじゃないかなーとも思います。
結果を返すTaskを作る
例えばファイルを読み込んで中身のテキストを返す非同期メソッドなど、
結果を呼び出し元に渡す必要がある場合は次のようにGenericで型を指定します。
using UnityEngine; using System.Threading; using System.Threading.Tasks; public class Example : MonoBehaviour { private async Task Start() { var result = await ExampleAsync(); Debug.Log(result); // 1 } // Genericで型を指定 private async Task<int> ExampleAsync() { await Task.Run(() => { Thread.Sleep(1000); }); // Genericで指定した型をreturnする return 1; } }
複数のTaskを直列で実行する
Taskを直列で実行するのは特に難しくないです。何回もawaitするだけです。
using UnityEngine; using System.Threading; using System.Threading.Tasks; public class Example : MonoBehaviour { private async Task Start() { for (int i = 0; i < 10; i++) { await ExampleAsync(i); } Debug.Log("end all."); } private async Task ExampleAsync(int index) { Debug.Log("start : " + index); await Task.Run(() => { Thread.Sleep(1000 * index); }); Debug.Log("end : " + index); } }
複数のTaskを並列で実行する
Taskを並列で処理するにはTask.WhenAll
を使う必要があります。
これによりリスト内のすべてのTaskの完了を待つことができます。
using UnityEngine; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; public class Example : MonoBehaviour { private async Task Start() { var tasks = new List<Task>(); for (int i = 0; i < 10; i++) { tasks.Add(ExampleAsync(i)); // この時点でそれぞれのTaskは開始する } await Task.WhenAll(tasks); // Task.WhenAll()ですべてのTaskの完了を待つ Debug.Log("end all."); } private async Task ExampleAsync(int index) { Debug.Log("start : " + index); await Task.Run(() => { Thread.Sleep(1000 * index); }); Debug.Log("end : " + index); } }
結果を受け取る場合はこんな感じです。
using UnityEngine; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; public class Example : MonoBehaviour { private async Task Start() { var tasks = new List<Task<int>>(); for (int i = 0; i < 10; i++) { tasks.Add(ExampleAsync(i)); } var results = await Task.WhenAll(tasks); foreach (var result in results) { Debug.Log(result); } } private async Task<int> ExampleAsync(int index) { Debug.Log("start : " + index); await Task.Run(() => { Thread.Sleep(1000 * index); }); Debug.Log("end : " + index); return index; } }
結果の型がバラバラな場合はこんな感じにするしかなさそうです。多分。
using UnityEngine; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; public class Example : MonoBehaviour { private async Task Start() { Task<int> taskInt = ExampleAsyncInt(); Task<float> taskFloat = ExampleAsyncFloat(); await Task.WhenAll(taskInt, taskFloat); Debug.Log(taskInt.Result); Debug.Log(taskFloat.Result); } private async Task<int> ExampleAsyncInt() { await Task.Run(() => Thread.Sleep(1000)); return 1; } private async Task<float> ExampleAsyncFloat() { await Task.Run(() => Thread.Sleep(1000)); return 0.01f; } }
Taskをキャンセルする
キャンセルの方法は少し独特です。
Cancel()
メソッドを持つCancellationTokenSource
のToken
を非同期メソッドに渡す- 非同期メソッド内でキャンセルチェックするタイミングで
CancellationToken.ThrowIfCancellationRequested();
を呼ぶ - キャンセルされていたら例外が発生するので非同期メソッド呼び出し元でキャッチ
コードを見てもらったほうが早そうです。
using UnityEngine; using System.Threading; using System.Threading.Tasks; using System; public class Example : MonoBehaviour { private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private async Task Start() { try { Debug.Log("start."); // キャンセルしたら例外を投げてもらえるようにCancellationToken.Tokenを渡す await ExampleAsync(_cancellationTokenSource.Token); Debug.Log("end."); } catch (OperationCanceledException e) { // キャンセル処理はここ if (e.CancellationToken == _cancellationTokenSource.Token) { Debug.Log("canceled."); } } } private async Task ExampleAsync(CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); // キャンセルチェック(この時点でキャンセルされていたら例外を投げる) for (int i = 0; i < 5; i++) { await Task.Run(() => Thread.Sleep(1000)); cancellationToken.ThrowIfCancellationRequested(); // キャンセルチェック(この時点でキャンセルされていたら例外を投げる) } } private void Update() { // Cを押したらキャンセル if (Input.GetKeyDown(KeyCode.C)) { // キャンセル処理 _cancellationTokenSource.Cancel(); } } private void OnDestroy() { // 破棄時にはキャンセル _cancellationTokenSource.Cancel(); } }
このあたりは次の記事を参考にさせていただきました。
例外処理について
例外処理は次のように書きます。
using UnityEngine; using System.Threading; using System.Threading.Tasks; using System; public class Example : MonoBehaviour { private async Task Start() { try { await ExampleAsync(); } catch { // Catchできる Debug.Log("catch."); } } private async Task ExampleAsync() { await Task.Run(() => Thread.Sleep(1000)); throw new Exception("example exception."); } }
ただawaitしてないとCatchできないっぽいです。
これは注意しないといけなそうです。
using UnityEngine; using System.Threading; using System.Threading.Tasks; using System; public class Example : MonoBehaviour { private void Start() { try { // awaitしてない var _ = ExampleAsync(); } catch { // Catchできない Debug.Log("catch."); } } private async Task ExampleAsync() { await Task.Run(() => Thread.Sleep(1000)); throw new Exception("example exception."); } }
このあたりは次の記事を参考にさせていただきました。
タイムアウト処理
タイムアウトはタイムアウト時間だけDelayするTaskを別で作って、
早く返ってきた方がどちらかで判定します。
using UnityEngine; using System.Threading; using System.Threading.Tasks; public class Example : MonoBehaviour { private async Task Start() { var timeoutMillis = 2000; var task = ExampleAsync(); if (await Task.WhenAny(task, Task.Delay(timeoutMillis)) == task) { Debug.Log("end."); } else { Debug.Log("timeout."); } } private async Task ExampleAsync() { await Task.Run(() => Thread.Sleep(3000)); } }
下記の記事ではCancellationTokenを使った方法も紹介されています。
どちらを使うかはケースバイケースな感じがします。
await後に元のスレッドに戻さない
Unityではawaitの前後で同じスレッドで処理が行われるようです。
using UnityEngine; using System.Threading; using System.Threading.Tasks; public class Example : MonoBehaviour { private async Task Start() { await ExampleAsync(); } private async Task ExampleAsync() { Debug.Log(Thread.CurrentThread.ManagedThreadId); // 1 await Task.Run(() => Thread.Sleep(1000)); Debug.Log(Thread.CurrentThread.ManagedThreadId); // 1 } }
Task.ConfigureAwait(false)
とするとTaskが実行されたスレッドでその後の処理が行われます。
using UnityEngine; using System.Threading; using System.Threading.Tasks; public class Example : MonoBehaviour { private async Task Start() { await ExampleAsync(); } private async Task ExampleAsync() { Debug.Log(Thread.CurrentThread.ManagedThreadId); await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false); // ConfigureAwait(false)にするとこのTaskと同じスレッドで Debug.Log(Thread.CurrentThread.ManagedThreadId); // これが実行される } }