await 演算子は、囲む async メソッドの実行をサスペンドします。これにより、呼び出し元スレッドは待機中に他の処理を実行できます。待機中の Task または Awaitable が完了したら、非同期コードを再開し、サスペンドした位置から実行を継続する必要があります。非同期コードがどのように再開されるかは、アプリケーションの機能とパフォーマンスに影響します。
待機開始時のコードの状態に関する情報は、同期コンテキストと呼ばれます。.NET プラットフォームは、このタイプの情報を取得するための SynchronizationContext クラスを提供します。Task の継続は、非同期メソッドが呼び出された同期コンテキストで実行されます。同期コンテキストが設定されていない場合は、スレッドプール を通じて実行されます。
ほとんどの Unity API はスレッドセーフではなく、メインスレッドからのみ呼び出すことができます。このため、Unity はデフォルトの SynchronizationContext をカスタムの UnitySynchronizationContext で上書きし、編集モードと再生モードの両方で、すべての .NET Task の継続が、デフォルトでメインスレッドで実行されるようにします。Unity のメインスレッドから Task を返すメソッドを呼び出すと、継続が UnitySynchronizationContext にポストされ、メインスレッドの次のフレームの Update 時に実行されます。バックグラウンドスレッドから呼び出した場合は、スレッドプールのスレッドで完了します。
同期コンテキストを取得することで、アプリケーションのパフォーマンスオーバーヘッドが増加し、メインスレッドの再開のために次のフレームの Update を待つことで、大きな待ち時間が発生します。これらの問題は、代わりに 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 の ジョブシステム を使用する方が安全です。ジョブシステムは複数のスレッドを安全に使用してジョブを並列実行し、マルチスレッドのパフォーマンスメリットを実現します。詳細については、ジョブシステムの概要 を参照してください。
Unity の Awaitable クラスは、以下のシナリオでジョブシステムよりも適しています。
ただし、計算負荷の高いアルゴリズムの並列化など、生存期間が短い処理には推奨されません。マルチコア 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);
}
}