まず、コルーチンについて話しましょう。それらは 2011 年に Unity に導入されましたが、.NET に async / await が登場する前でさえありました。 Unity では、コルーチンを使用すると、一度にすべての命令を実行するのではなく、複数のフレームにわたって一連の命令を実行できます。これらはスレッドに似ていますが、軽量で Unity の更新ループに統合されているため、ゲーム開発に適しています。
コルーチンを作成するには、戻り値の型がIEnumerator
の関数を宣言する必要があります。この関数には、コルーチンで実行する任意のロジックを含めることができます。
コルーチンを開始するには、 MonoBehaviour
インスタンスでStartCoroutine
メソッドを呼び出し、コルーチン関数を引数として渡す必要があります。
public class Example : MonoBehaviour { void Start() { StartCoroutine(MyCoroutine()); } IEnumerator MyCoroutine() { Debug.Log("Starting coroutine"); yield return null; Debug.Log("Executing coroutine"); yield return null; Debug.Log("Finishing coroutine"); } }
Unity には、 WaitForSeconds
、 WaitForEndOfFrame
、 WaitForFixedUpdate
、 WaitForSecondsRealtime
、 WaitUntil
など、いくつかの yield 命令が用意されています。それらを使用すると割り当てが発生することを覚えておくことが重要であるため、可能な限り再利用する必要があります。
たとえば、ドキュメントの次のメソッドを検討してください。
IEnumerator Fade() { Color c = renderer.material.color; for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return new WaitForSeconds(.1f); } }
ループが繰り返されるたびに、 new WaitForSeconds(.1f)
の新しいインスタンスが作成されます。これの代わりに、作成をループの外に移動して、割り当てを回避できます。
IEnumerator Fade() { Color c = renderer.material.color; **var waitForSeconds = new WaitForSeconds(0.2f);** for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return **waitForSeconds**; } }
注意すべきもう 1 つの重要なプロパティは、 AsyncOperation
がYieldInstruction
の子孫であるため、Unity が提供するすべてのAsync
メソッドでyield return
を使用できることです。
yield return SceneManager.LoadSceneAsync("path/to/scene.unity");
MonoBehaviour
に厳密に関連付けられています。 GameObject
がオフまたは破棄されると、コルーチンの処理が停止します。try-catch-finally
構造は使用できません。yield return
の後、少なくとも 1 つのフレームが通過します。Promise は、非同期操作を整理して読みやすくするためのパターンです。多くのサードパーティの JavaScript ライブラリで使用されているため人気があり、ES6 以降ではネイティブに実装されています。
Promise を操作する主な方法は、コールバック関数を使用することです。
Promises/A+ 組織これらのによると、Promise は次の 3 つの状態のいずれかになります。
Pending
: 初期状態。これは、非同期操作がまだ進行中であり、操作の結果がまだ不明であることを意味します。Fulfilled
( Resolved
): 解決された状態には、操作の結果を表す値が伴います。Rejected
: 非同期操作が何らかの理由で失敗した場合、Promise は「拒否された」と言われます。拒否された状態には、失敗の理由が伴います。
var promise = MakeRequest("//some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception));
public IPromise<string> MakeRequest(string url) { // Create a new promise object var promise = new Promise<string>(); // Create a new web client using var client = new WebClient(); // Add a handler for the DownloadStringCompleted event client.DownloadStringCompleted += (sender, eventArgs) => { // If an error occurred, reject the promise if (eventArgs.Error != null) { promise.Reject(eventArgs.Error); } // Otherwise, resolve the promise with the result else { promise.Resolve(eventArgs.Result); } }; // Start the download asynchronously client.DownloadStringAsync(new Uri(url), null); // Return the promise return promise; }
Promise
でコルーチンをラップすることもできます:
void Start() { // Load the scene and then show the intro animation LoadScene("path/to/scene.unity") .Then(() => ShowIntroAnimation()) .Then( ... ); } // Load a scene and return a promise Promise LoadScene(string sceneName) { // Create a new promise var promise = new Promise(); // Start a coroutine to load the scene StartCoroutine(LoadSceneRoutine(promise, sceneName)); // Return the promise return promise; } IEnumerator LoadSceneRoutine(Promise promise, string sceneName) { // Load the scene asynchronously yield return SceneManager.LoadSceneAsync(sceneName); // Resolve the promise once the scene is loaded promise.Resolve(); }
そしてもちろん、 ThenAll
/ Promise.All
とThenRace
/ Promise.Race
を使用して、Promise の実行順序を自由に組み合わせて整理できます。
// Execute the following two promises in sequence Promise.Sequence( () => Promise.All( // Execute the two promises in parallel RunAnimation("Foo"), PlaySound("Bar") ), () => Promise.Race( // Execute the two promises in a race RunAnimation("One"), PlaySound("Two") ) );
.NET の歴史では、次の段階を区別できます。
BeginSmth
メソッドはIAsyncResult
インターフェイスを返します。 EndSmth
メソッドはIAsyncResult
を受け取ります。 EndSmth
呼び出し時に操作が完了していない場合、スレッドはブロックされます。Task
およびTask<TResult>
の導入によって改善されました。
非同期メソッドを作成するには、メソッドをキーワードasync
でマークし、内部にawait
を含め、戻り値をTask
、 Task<T>
またはvoid
にする必要があります (推奨されません)。
public async Task Example() { SyncMethodA(); await Task.Delay(1000); // the first async operation SyncMethodB(); await Task.Delay(2000); // the second async operation SyncMethodC(); await Task.Delay(3000); // the third async operation }
この例では、実行は次のように行われます。
SyncMethodA
) の呼び出しに先行するコードが実行されます。await Task.Delay(1000)
が起動され、実行されることが期待されます。一方、非同期操作が完了したとき (「継続」) に呼び出されるコードは保存されます。SyncMethodB
) までのコードが実行を開始します。await Task.Delay(2000)
) が開始され、実行されることが期待されます。同時に、継続 — 2 番目の非同期操作 ( SyncMethodC
) に続くコードが保持されます。SyncMethodC
が実行され、続いて 3 番目の非同期操作が実行され、 await Task.Delay(3000)
待機します。
WhenAll
とWhenAny
を使用して、実行順序の任意の組み合わせを編成することもできます。
var allTasks = Task.WhenAll( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); allTasks.ContinueWith(t => { Console.WriteLine("All the tasks are completed"); }); var anyTask = Task.WhenAny( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); anyTask.ContinueWith(t => { Console.WriteLine("One of tasks is completed"); });
C#コンパイラは、async/await 呼び出しをIAsyncStateMachine
ステート マシンに変換します。これは、非同期操作を完了するために実行する必要がある一連のアクションです。
したがって、 Example
メソッドは、注釈[AsyncStateMachine(typeof(ExampleStateMachine))]
を使用してステート マシンを作成および初期化するように変換され、ステート マシン自体には、await 呼び出しの数に等しい数の状態があります。
変換されたメソッドの例Example
[AsyncStateMachine(typeof(ExampleStateMachine))] public /*async*/ Task Example() { // Create a new instance of the ExampleStateMachine class ExampleStateMachine stateMachine = new ExampleStateMachine(); // Create a new AsyncTaskMethodBuilder and assign it to the taskMethodBuilder property of the stateMachine instance stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); // Set the currentState property of the stateMachine instance to -1 stateMachine.currentState = -1; // Start the stateMachine instance stateMachine.taskMethodBuilder.Start(ref stateMachine); // Return the Task property of the taskMethodBuilder return stateMachine.taskMethodBuilder.Task; }
生成されたステート マシンの例ExampleStateMachine
[CompilerGenerated] private sealed class ExampleStateMachine : IAsyncStateMachine { public int currentState; public AsyncTaskMethodBuilder taskMethodBuilder; private TaskAwaiter taskAwaiter; public int paramInt; private int localInt; void IAsyncStateMachine.MoveNext() { int num = currentState; try { TaskAwaiter awaiter3; TaskAwaiter awaiter2; TaskAwaiter awaiter; switch (num) { default: localInt = paramInt; // Call the first synchronous method SyncMethodA(); // Create a task awaiter for a delay of 1000 milliseconds awaiter3 = Task.Delay(1000).GetAwaiter(); // If the task is not completed, set the current state to 0 and store the awaiter if (!awaiter3.IsCompleted) { currentState = 0; taskAwaiter = awaiter3; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); return; } // If the task is completed, jump to the label after the first await goto Il_AfterFirstAwait; case 0: // Retrieve the awaiter from the taskAwaiter field awaiter3 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the first await goto Il_AfterFirstAwait; case 1: // Retrieve the awaiter from the taskAwaiter field awaiter2 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the second await goto Il_AfterSecondAwait; case 2: // Retrieve the awaiter from the taskAwaiter field awaiter = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; break; Il_AfterFirstAwait: awaiter3.GetResult(); // Call the second synchronous method SyncMethodB(); // Create a task awaiter for a delay of 2000 milliseconds awaiter2 = Task.Delay(2000).GetAwaiter(); // If the task is not completed, set the current state to 1 and store the awaiter if (!awaiter2.IsCompleted) { currentState = 1; taskAwaiter = awaiter2; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine); return; } // If the task is completed, jump to the label after the second await goto Il_AfterSecondAwait; Il_AfterSecondAwait: // Get the result of the second awaiter awaiter2.GetResult(); // Call the SyncMethodC SyncMethodC(); // Create a new awaiter with a delay of 3000 milliseconds awaiter = Task.Delay(3000).GetAwaiter(); // If the awaiter is not completed, set the current state to 2 and store the awaiter if (!awaiter.IsCompleted) { currentState = 2; taskAwaiter = awaiter; // Set the stateMachine to this ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } break; } // Get the result of the awaiter awaiter.GetResult(); } catch (Exception exception) { currentState = -2; taskMethodBuilder.SetException(exception); return; } currentState = -2; taskMethodBuilder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { /*...*/ } }
AwaitUnsafeOnCompleted
呼び出しでは、現在の同期コンテキストSynchronizationContext
が取得されます。 SynchronizationContext は、一連の非同期操作の実行を制御するコンテキストを表すために使用される C# の概念です。複数のスレッド間でコードの実行を調整し、コードが特定の順序で実行されるようにするために使用されます。 SynchronizationContext の主な目的は、マルチスレッド環境での非同期操作のスケジューリングと実行を制御する方法を提供することです。
環境が異なれば、 SynchronizationContext
の実装も異なります。たとえば、.NET には次のようなものがあります。
System.Windows.Threading.DispatcherSynchronizationContext
System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.WinRTSynchronizationContext
System.Web.AspNetSynchronizationContext
Unity には独自の同期コンテキストUnitySynchronizationContext
もあり、PlayerLoop API へのバインディングで非同期操作を使用できます。次のコード例は、 Task.Yield()
を使用して各フレームでオブジェクトを回転する方法を示しています。
private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } }
using UnityEngine; using System.Net.Http; using System.Threading.Tasks; public class NetworkRequestExample : MonoBehaviour { private async void Start() { string response = await GetDataFromAPI(); Debug.Log("Response from API: " + response); } private async Task<string> GetDataFromAPI() { using (var client = new HttpClient()) { var response = await client.GetStringAsync("//api.example.com/data"); return response; } } }
UnitySynchronizationContext
のおかげで、このコードの実行はメインの Unity スレッドで続行されるため、非同期操作が完了した直後にUnityEngine
メソッド ( Debug.Log()
など) を安全に使用できます。
このクラスを使用すると、 Task
オブジェクトを管理できます。これは、古い非同期メソッドを TAP に適応させるために作成されましたが、何らかのイベントで長時間実行される操作をTask
でラップしたい場合にも非常に役立ちます。
次の例では、 taskCompletionSource
内のTask
オブジェクトは開始から 3 秒後に完了し、その結果をUpdate
メソッドで取得します。
using System.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private TaskCompletionSource<int> taskCompletionSource; private void Start() { // Create a new TaskCompletionSource taskCompletionSource = new TaskCompletionSource<int>(); // Start a coroutine to wait 3 seconds // and then set the result of the TaskCompletionSource StartCoroutine(WaitAndComplete()); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the TaskCompletionSource taskCompletionSource.SetResult(10); } private async void Update() { // Await the result of the TaskCompletionSource int result = await taskCompletionSource.Task; // Log the result to the console Debug.Log("Result: " + result); } }
C# では、キャンセル トークンを使用して、タスクまたは操作をキャンセルする必要があることを通知します。トークンはタスクまたは操作に渡され、タスクまたは操作内のコードはトークンを定期的にチェックして、タスクまたは操作を停止する必要があるかどうかを判断できます。これにより、タスクまたは操作を突然強制終了するのではなく、クリーンで適切なキャンセルが可能になります。
全体的なパターンは、 TaskCompletionSource
の使用に似ています。まず、 CancellationTokenSource
が作成され、次にそのToken
が非同期操作に渡されます。
public class ExampleMonoBehaviour : MonoBehaviour { private CancellationTokenSource _cancellationTokenSource; private async void Start() { // Create a new CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); // Get the token from the CancellationTokenSource CancellationToken token = _cancellationTokenSource.Token; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } private void OnDestroy() { // Cancel the token when the object is destroyed _cancellationTokenSource.Cancel(); } }
操作がキャンセルされると、 OperationCanceledException
がスローされ、 Task.IsCanceled
プロパティがtrue
に設定されます。
Task
オブジェクトは Unity ではなく .NET ランタイムによって管理されることに注意することが重要です。タスクを実行しているオブジェクトが破棄された場合 (またはエディターでゲームがプレイ モードを終了した場合)、タスクは Unity と同じように実行され続けます。キャンセルするわけにはいきません。
対応するCancellationToken
をawait Task
に常に添付する必要があります。これはコードの冗長性につながり、Unity 2022.2 ではMonoBehaviour
レベルとApplication
レベル全体でビルトイン トークンが登場しました。
MonoBehaviour
オブジェクトのdestroyCancellationToken
を使用すると、前の例がどのように変化するかを見てみましょう。
using System.Threading; using System.Threading.Tasks; using UnityEngine; public class ExampleMonoBehaviour : MonoBehaviour { private async void Start() { // Get the cancellation token from the MonoBehaviour CancellationToken token = this.destroyCancellationToken; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } }
CancellationTokenSource
手動で作成し、 OnDestroy
メソッドでタスクを完了する必要はなくなりました。特定のMonoBehaviour
に関連付けられていないタスクについては、 UnityEngine.Application.exitCancellationToken
を使用できます。これにより、再生モード (エディタ内) を終了するとき、またはアプリケーションを終了するときにタスクが終了します。
Task
オブジェクトは扱いにくく、多くの割り当てが発生します。Task
が Unity スレッド (シングルスレッド) に対応していません。
ライブラリは、スレッドやSynchronizationContext
を使用せずにこれらの制限をバイパスします。 UniTask<T>
構造体ベースの型を使用することで、割り当てがないことを実現します。
また、拡張メソッドを使用して、すべてのAsyncOperations
UnitTask
に変換できます。
using UnityEngine; using UniTask; public class AssetLoader : MonoBehaviour { public async void LoadAsset(string assetName) { var loadRequest = Resources.LoadAsync<GameObject>(assetName); await loadRequest.AsUniTask(); var asset = loadRequest.asset as GameObject; if (asset != null) { // Do something with the loaded asset } } }
この例では、 LoadAsset
メソッドはResources.LoadAsync
を使用してアセットを非同期的に読み込みます。次に、 AsUniTask
メソッドを使用して、 LoadAsync
によって返されたAsyncOperation
を待機可能なUniTask
に変換します。
以前と同様に、 UniTask.WhenAll
とUniTask.WhenAny
を使用して、実行順序の任意の組み合わせを編成できます。
using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private async void Start() { // Start two Tasks and wait for both to complete await UniTask.WhenAll(Task1(), Task2()); // Start two Tasks and wait for one to complete await UniTask.WhenAny(Task1(), Task2()); } private async UniTask Task1() { // Do something } private async UniTask Task2() { // Do something } }
UniTask には、パフォーマンスを向上させるためにUnitySynchronizationContext
を置き換えるために使用できるUniTaskSynchronizationContext
と呼ばれるSynchronizationContext
の別の実装があります。
Unity 2023.1 の最初のアルファ版では、 Awaitable
クラスが導入されました。待機可能なコルーチンは、Unity で実行するように設計された非同期/待機互換のタスクのようなタイプです。 .NET タスクとは異なり、ランタイムではなくエンジンによって管理されます。
private async Awaitable DoSomethingAsync() { // awaiting built-in events await Awaitable.EndOfFrameAsync(); await Awaitable.WaitForSecondsAsync(); // awaiting .NET Tasks await Task.Delay(2000, destroyCancellationToken); await Task.Yield(); // awaiting AsyncOperations await SceneManager.LoadSceneAsync("path/to/scene.unity"); // ... }
これらは、非同期メソッドの戻り値の型として待機して使用できます。 System.Threading.Tasks
と比較すると、これらは洗練されていませんが、Unity 固有の仮定に基づいてパフォーマンスを向上させるショートカットを使用します。
Awaitable
オブジェクトは 1 回だけ待機できます。複数の非同期関数で待機することはできません。Awaiter.GetResults()
完了するまでブロックしません。操作が完了する前に呼び出すと、未定義の動作になります。ExecutionContext
をキャプチャしないでください。セキュリティ上の理由から、.NET タスクは、非同期呼び出し間で偽装コンテキストを伝達するために、待機中に実行コンテキストをキャプチャします。SynchronizationContext
をキャプチャしないでください。コルーチンの継続は、完了を発生させたコードから同期的に実行されます。ほとんどの場合、これは Unity メイン フレームからのものです。ObjectPool
改善され、非同期ステート マシンによって生成される一般的な取得/解放シーケンスでのStack<T>
境界チェックが回避されました。
時間のかかる操作の結果を取得するには、 Awaitable<T>
型を使用できます。 TaskCompletitionSource
と同様に、 AwaitableCompletionSource
およびAwaitableCompletionSource<T>
を使用してAwaitable
の完了を管理できます。
using UnityEngine; using Cysharp.Threading.Tasks; public class ExampleBehaviour : MonoBehaviour { private AwaitableCompletionSource<bool> _completionSource; private async void Start() { // Create a new AwaitableCompletionSource _completionSource = new AwaitableCompletionSource<bool>(); // Start a coroutine to wait 3 seconds // and then set the result of the AwaitableCompletionSource StartCoroutine(WaitAndComplete()); // Await the result of the AwaitableCompletionSource bool result = await _completionSource.Awaitable; // Log the result to the console Debug.Log("Result: " + result); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the AwaitableCompletionSource _completionSource.SetResult(true); } }
ゲームのフリーズにつながる大規模な計算を実行する必要がある場合があります。このためには、 Awaitable メソッドを使用することをお勧めします: BackgroundThreadAsync()
およびMainThreadAsync()
。メインスレッドを終了してメインスレッドに戻ることができます。
private async Awaitable DoCalculationsAsync() { // Awaiting execution on a ThreadPool background thread. await Awaitable.BackgroundThreadAsync(); var result = PerformSomeHeavyCalculations(); // Awaiting execution on the Unity main thread. await Awaitable.MainThreadAsync(); // Using the result in main thread Debug.Log(result); }
団結 | コルーチン | 約束 | .NET タスク | ユニタスク | 組み込みのキャンセル トークン | 待望のAPI |
---|---|---|---|---|---|---|
5.6 | ✅ | ✅ | | | | |
2017.1 | ✅ | ✅ | ✅ | | | |
2018.4 | ✅ | ✅ | ✅ | ✅ | | |
2022.2 | ✅ | ✅ | ✅ | ✅ | ✅ | |
2023.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |