ドキュメント

Unity Engine

Unity Engine

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ヶ月前

await
演算子は、囲む async メソッドの実行をサスペンドします。これにより、呼び出し元スレッドは待機中に他の処理を実行できます。待機中の
Task
または
Awaitable
が完了したら、非同期コードを再開し、サスペンドした位置から実行を継続する必要があります。非同期コードがどのように再開されるかは、アプリケーションの機能とパフォーマンスに影響します。

.NET Task の継続

待機開始時のコードの状態に関する情報は、同期コンテキストと呼ばれます。.NET プラットフォームは、このタイプの情報を取得するための
SynchronizationContext
クラスを提供します。
Task
の継続は、非同期メソッドが呼び出された同期コンテキストで実行されます。同期コンテキストが設定されていない場合は、スレッドプール を通じて実行されます。
ほとんどの Unity API はスレッドセーフではなく、メインスレッドからのみ呼び出すことができます。このため、Unity はデフォルトの
SynchronizationContext
をカスタムの
UnitySynchronizationContext
で上書きし、編集モードと再生モードの両方で、すべての .NET
Task
の継続が、デフォルトでメインスレッドで実行されるようにします。Unity のメインスレッドから
Task
を返すメソッドを呼び出すと、継続が
UnitySynchronizationContext
にポストされ、メインスレッドの次のフレームの
Update
時に実行されます。バックグラウンドスレッドから呼び出した場合は、スレッドプールのスレッドで完了します。
同期コンテキストを取得することで、アプリケーションのパフォーマンスオーバーヘッドが増加し、メインスレッドの再開のために次のフレームの Update を待つことで、大きな待ち時間が発生します。これらの問題は、代わりに
Awaitable
を使用することで回避できます。

Awaitable の継続

特に記述がない限り、Unity API によって返されるすべての
Awaitable
インスタンスはメインスレッドで完了します。そのため、同期コンテキストを取得する必要はありません。
Awaitable
の継続は、処理が完了すると同期して実行されます。つまり、コードはすぐに再開でき、次のフレームを待つ必要はありません。
デフォルトの継続動作を
Awaitable.BackgroundThreadAsync
で明示的にオーバーライドできます。以下の例では、
Awaitable
がバックグラウンドスレッドで完了するように指定します。
private 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}
この場合、
Awaitable
がバックグラウンドスレッドで完了するように設定すると、問題が発生します。
Start
メソッドがバックグラウンドスレッドで再開すると、次の
SceneManager.LoadSceneAsync
の呼び出しは失敗します。これは、
SceneManager.LoadSceneAsync
がメインスレッドでのみ機能するためです。
この状況を修正するには、
Awaitable.MainThreadAsync
を使用して、
Awaitable
を返すメソッドをデフォルトでメインスレッドで完了させ、バックグラウンドスレッド処理のオプションも提供します。以下の例では、
DoHeavyComputationInBackgroundAsync
はデフォルトでメインスレッドで完了する前に、バックグラウンドスレッドでいくつかの処理を行います。
continueOnMainThread
パラメーターは、負荷の高い演算処理を連鎖させるときなど、必要に応じてバックグラウンドスレッドで完了するオプションを呼び出し元に提供します。
private 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()
はバックグラウンドスレッドから呼び出すのが最も効率的です。どちらの場合も、完了直後にコードが再開されるためです。
MainThreadAsync
でバックグラウンドスレッドからメインスレッドに切り替えると、メインスレッドの次の フレーム更新 までコードは再開できません。
Task
を返す API をメインスレッドから呼び出し、それが同期的に完了しない場合、継続の実行には少なくとも次の
Update
(30 fps で 33 ミリ秒) を待つ必要があります。ネットワークの待ち時間が問題になる場合は、これをメインスレッド外で実行し、カスタムロジックを使用してメインスレッドとネットワークタスクを同期することをお勧めします。
開発ビルドで、マルチスレッドコードで 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 を最大限に活用してアルゴリズムを並列化するには、代わりに ジョブシステム を使用します。

コードからの完了のトリガー

AwaitableCompletionSource
と
AwaitableCompletionSource<T>
を使用すると、ユーザーコードから完了を発生させる
Awaitable
インスタンスを作成できます。例えば、ユーザーインタラクションの終了を待つステートマシンを実装せずに、ユーザープロンプトを実装できます。
public 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); }}

追加リソース