Awaitable クラスは待機可能なカスタムの Unity 型で、C# 非同期プログラミングモデルで async の戻り値の型として使用できます。以下を含む Unity の非同期 API のほとんどは、async と await のパターンをサポートします。
NextFrameAsync、WaitForSecondsAsync、EndOfFrameAsync、FixedUpdateAsync
AsyncOperation を継承するすべての型以下のように、Awaitable クラスを await 演算子と async の戻り値の型の両方とともに独自のコードで使用できます。
async Awaitable<List<Achievement>> GetAchievementsAsync()
{
var apiResult = await SomeMethodReturningATask(); // or any await-compatible type
List<Achievement> achievements = JsonConvert.DeserializeObject<List<Achievement>>(apiResult);
return achievements;
}
async Awaitable ShowAchievementsView()
{
ShowLoadingOverlay();
List<Achievement> achievements = await GetAchievementsAsync();
HideLoadingOverlay();
ShowAchivementsList(achievements);
}
Awaitable は、Unity プロジェクトの非同期コードに、.NET Task に代わるより効率的な方法を提供するように設計されています。Awaitable は効率的ですが、Task と比較して重要な制限があります。
最も重要な制限は、Awaitable インスタンスが割り当てを制限するためにプールされることです。以下の例を考えてみてください。
class SomeMonoBehaviorWithAwaitable : MonoBehavior
{
public async void Start()
{
while(true)
{
// do some work on each frame
await Awaitable.NextFrameAsync();
}
}
}
この例の MonoBehavior の各インスタンスは、プーリングを行わないと、フレームごとに Awaitable オブジェクトを割り当て、ガベージコレクターのワークロード を増加させ、パフォーマンスを低下させます。これを軽減するために、Unity は待機後に Awaitable オブジェクトを内部 Awaitable プールに返します。
重要:Awaitable インスタンスのプーリングは、1 つの Awaitable インスタンスを複数回 await することは決して安全ではないことを意味します。これを行うと、例外やデッドロックなどの未定義の動作につながる可能性があります。
.NET ValueTask<TResult> は、Awaitable と同じ主なメリットと制限をいくつか提供します。ValueTask の推奨される一般的な使用法は、ほとんどの場合に同期的に完了すると予想される非同期ワークロードです。詳細については、Understanding the Whys, Whats, and Whens of ValueTask を参照してください。
以下の表は、Unity の Awaitable クラスと .NET Task および ValueTask の機能比較をまとめたものです。
| 機能 | Task |
ValueTask |
UnityEngine.Awaitable |
|---|---|---|---|
| 必要な割り当て |
多い。Task を返すメソッドの呼び出しごとに割り当てられ、メモリ使用量とガベージコレクターのワークロードが増加します。 |
必要に応じて。 プーリングを使用して最適化できます。 |
必要最小限。Awaitable を返すメソッドを呼び出しても、通常はメモリは割り当てられません。Awaitable インスタンスはデフォルトでプールされるためです。 |
| 安全に複数回待機可能 | 可。 |
不可。ValueTask.AsTask で Task に変換する必要があります。 |
不可。 カスタムの AsTask 拡張メソッドで Task に変換する必要があります。コード例リファレンスの 同じメソッド内での複数回の await を参照してください。 |
| 継続の非同期実行 |
可。 同期コンテキストをデフォルトで使用します。それ以外の場合は ThreadPool を使用します。Unity のメインスレッドで完了するときに待ち時間が長くなります。これは、コードが再開されるために次のフレームの Update を待機する必要があるためです。 |
可。 待機中のタスクが同期的に完了するケースに最適化されています。非同期的に完了する場合、継続動作は Task と同じです。 |
不可。 継続は、完了がトリガーされると同期的に実行されます。つまり、コードは、完了がトリガーされたのと同じフレーム内ですぐに再開されます。詳細については、Awaitable の完了と継続 を参照してください。 |
| コードによって完了をトリガー可能 |
可。TaskCompletionSource を使用します。 |
一般的なユースケース (ほとんどの場合に同期的に完了するタスク) には適用されません。 |
可。AwaitableCompletionSource を使用します。 |
| 値を返すことが可能 |
可。Task<TResult> を使用します。 |
可。ValueTask<TResult> を使用します。 |
可。UnityEngine.Awaitable<T> を使用します。 |
WaitAll および WaitAny のビルトインサポート |
可。 |
不可。ValueTask.AsTask で Task に変換する必要があります。 |
不可。 カスタムの AsTask 拡張メソッドで Task に変換する必要があります。コード例リファレンスの .NET タスクでの Awaitable のラッピング を参照してください。 |
| Unity スレッドと更新ループに対応した実行スケジュール | 不可。 | 不可。 |
可。Awaitable.BackgroundThreadAsync と Awaitable.MainThreadAsync を使用して、Awaitable を再開するスレッドを指定できます。また、Awaitable.NextFrameAsync と Awaitable.FixedUpdateAsync を使用して、Update または FixedUpdate ループに関連して処理をスケジュールすることもできます。詳細については、Awaitable の完了と継続 を参照してください。 |
API の選択は非同期コードのパフォーマンスプロファイルによって異なりますが、一般的には以下のように選択します。
Task は、複数回待機する必要がある場合や、複数のコンシューマーから同時に待機する必要がある場合の唯一の選択肢です。ValueTask は、ほとんどの場合に同期的に完了する高スループットの非同期コードがある場合に適しています。Awaitable は、以下のような場合に適しています。
Awaitable コルーチンは、通常、イテレーターベースのコルーチンよりも効率的です。特に、イテレーターが WaitForFixedUpdate などの null 以外の値を返す場合に適しています。
ただし、多くの Awaitable コルーチンを同時に実行すると、パフォーマンス上のメリットが低下します。例えば、前述のコード例のような MonoBehaviour は、while ループで Awaitable.NextFrameAsync を待機しますが、大規模なプロジェクト内のすべてのゲームオブジェクトにアタッチされると、パフォーマンスの問題が発生する可能性があります。