Awaitable の完了と継続
Understand how asynchronous code resumes on completion of an awaited task and how this affects the function and performance of your application.
読み終わるまでの所要時間 12 分最終更新 1ヶ月前
awaitTaskAwaitable.NET Task の継続
待機開始時のコードの状態に関する情報は、同期コンテキストと呼ばれます。.NET プラットフォームは、このタイプの情報を取得するための クラスを提供します。 の継続は、非同期メソッドが呼び出された同期コンテキストで実行されます。同期コンテキストが設定されていない場合は、スレッドプール を通じて実行されます。
SynchronizationContextTaskほとんどの Unity API はスレッドセーフではなく、メインスレッドからのみ呼び出すことができます。このため、Unity はデフォルトの をカスタムの で上書きし、編集モードと再生モードの両方で、すべての .NET の継続が、デフォルトでメインスレッドで実行されるようにします。Unity のメインスレッドから を返すメソッドを呼び出すと、継続が にポストされ、メインスレッドの次のフレームの 時に実行されます。バックグラウンドスレッドから呼び出した場合は、スレッドプールのスレッドで完了します。
SynchronizationContextUnitySynchronizationContextTaskTaskUnitySynchronizationContextUpdate同期コンテキストを取得することで、アプリケーションのパフォーマンスオーバーヘッドが増加し、メインスレッドの再開のために次のフレームの Update を待つことで、大きな待ち時間が発生します。これらの問題は、代わりに を使用することで回避できます。
AwaitableAwaitable の継続
特に記述がない限り、Unity API によって返されるすべての インスタンスはメインスレッドで完了します。そのため、同期コンテキストを取得する必要はありません。 の継続は、処理が完了すると同期して実行されます。つまり、コードはすぐに再開でき、次のフレームを待つ必要はありません。
AwaitableAwaitableデフォルトの継続動作を で明示的にオーバーライドできます。以下の例では、 がバックグラウンドスレッドで完了するように指定します。
Awaitable.BackgroundThreadAsyncAwaitableprivate async Awaitable<float> DoHeavyComputationInBackgroundAsync(){ await Awaitable.BackgroundThreadAsync(); // do some heavy math here return 42; // this will make the returned awaitable complete on a background thread}public async Awaitable Start(){ var computationResult = await DoHeavyComputationInBackgroundAsync(); await SceneManager.LoadSceneAsync("my-scene"); // this will fail as SceneManager.LoadAsync only works from main thread}
この場合、 がバックグラウンドスレッドで完了するように設定すると、問題が発生します。 メソッドがバックグラウンドスレッドで再開すると、次の の呼び出しは失敗します。これは、 がメインスレッドでのみ機能するためです。
AwaitableStartSceneManager.LoadSceneAsyncSceneManager.LoadSceneAsyncこの状況を修正するには、 を使用して、 を返すメソッドをデフォルトでメインスレッドで完了させ、バックグラウンドスレッド処理のオプションも提供します。以下の例では、 はデフォルトでメインスレッドで完了する前に、バックグラウンドスレッドでいくつかの処理を行います。 パラメーターは、負荷の高い演算処理を連鎖させるときなど、必要に応じてバックグラウンドスレッドで完了するオプションを呼び出し元に提供します。
Awaitable.MainThreadAsyncAwaitableDoHeavyComputationInBackgroundAsynccontinueOnMainThreadprivate async Awaitable<float> DoHeavyComputationInBackgroundAsync(bool continueOnMainThread = true){ await Awaitable.BackgroundThreadAsync(); // do some heavy math here float result = 42; // by default, switch back to main thread: if(continueOnMainThread){ await Awaitable.MainThreadAsync(); } return result;}public async Awaitable Start(){ var computationResult = await DoHeavyComputationInBackgroundAsync(); await SceneManager.LoadSceneAsync("my-scene"); // this is ok!}
**注:**Unity は、再生モード終了時にバックグラウンドで実行中のコードを自動的に停止しません。再生モード終了時にバックグラウンド処理をキャンセルするには、 を使用します。
Application.exitCancellationTokenスレッド切り替えとパフォーマンス
await Awaitable.MainThreadAsync()await Awaitable.BackgroundThreadAsync()MainThreadAsyncTaskUpdate開発ビルドで、マルチスレッドコードで Unity API を使用しようとすると、Unity は以下のエラーメッセージを表示します。
UnityException: Internal_CreateGameObject can only be called from the main thread.Constructors and field initializers will be executed from the loading thread when loading a scene.Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
重要: パフォーマンス上の理由から、Unity は 非開発ビルド でマルチスレッド動作をチェックしません。また、ライブビルドではこのエラーを表示しません。Unity はこれらのコンテキストでマルチスレッドコードの実行を妨げませんが、実際に複数のスレッドを使用した場合、クラッシュやその他の予測できないエラーが発生する可能性があります。独自のマルチスレッドを使用する代わりに、Unity の ジョブシステム を使用する方が安全です。ジョブシステムは複数のスレッドを安全に使用してジョブを並列実行し、マルチスレッドのパフォーマンスメリットを実現します。詳細については、ジョブシステムの概要 を参照してください。
Awaitable とジョブシステムの比較
Unity の クラスは、以下のシナリオでジョブシステムよりも適しています。
Awaitable- ファイルの操作やウェブリクエストの実行など、本質的に非同期的な処理を扱うときのコードを、ノンブロッキングな方法で簡略化する。
- 長時間実行されるタスク (1 フレーム超) をバックグラウンドスレッドにオフロードする。
- イテレーターベースのコルーチンを最新化する。
- 複数の種類の非同期処理 (フレームイベント、Unity イベント、サードパーティ製の非同期 API、I/O) を待機する。
ただし、計算負荷の高いアルゴリズムの並列化など、生存期間が短い処理には推奨されません。マルチコア CPU を最大限に活用してアルゴリズムを並列化するには、代わりに ジョブシステム を使用します。
コードからの完了のトリガー
AwaitableCompletionSourceAwaitableCompletionSource<T>Awaitablepublic class UserNamePrompt : MonoBehavior { TextField _userNameTextField; AwaitableCompletionSource<string> _completionSource = new AwaitableCompletionSource<string>(); public void Start() { var rootVisual = GetComponent<UIDocument>().rootVisualElement; var userNameField = rootVisual.Q<TextField>("userNameField"); rootVisual.Q<Button>("OkButton").clicked += ()=>{ _completionSource.SetResult(userNameField.text); } } public Awaitable<string> WaitForUsernameAsync() => _completionSource.Awaitable;}...public class HighScoreRanks : MonoBehavior { ... public async Awaitable ReportCurrentUserScoreAsync(int score) { _userNameOverlayGameObject.SetActive(true); var prompt = _userNameOverlayGameObject.GetComponent<UserNamePrompt>(); var userName = await prompt.WaitForUsernameAsync(); _userNameOverlayGameObject.SetActive(false); await SomeAPICall(userName, score); }}