Unity Async非同期チュートリアル、結局.Netネイティブと何が違うのか?
Async/Awaitが何かについては説明しません。コルーチンに似ていますがより簡潔で、ここではあなたが現代的な非同期プログラミングの知識を持っていることを前提とします。
Unityの内部実装をまとめて、結局何をしているのかを理解しやすくします。簡単に言うと、Unity内部ではいくつかの処理が行われ、呼び出し元がメインスレッドである場合に2点が保証されます:1. 呼び出し元でスレッド切り替えが発生しない、2. 呼び出された非同期関数が他のスレッドで実行されない。
UnityのAsyncモードの目標
Asyncモードはより現代的で、将来の方向性ですが、Unityは現在、Asyncでコルーチン(IEnumerators)を置き換えるつもりはなく、主にIOクラスなどの操作に使用することを目的としています。
Unity Asyncはコルーチンより高性能だが、並列数は低い
シングルトン/メインクラスオブジェクトでの使用に適しています。例えば、カメラ、UI、IOメソッドなどです。少量のAsyncはコルーチンより高性能ですが、大量のObjectが並列実行する必要があるコードでは、依然としてコルーチンを使用すべきです。
性能部分
可能な限りUnityでラップされたAwaitableを返し、Taskは返さない(Unity6)
.NetのAsyncはTaskを使用して行われます。以下の通りです:
async Task function() {
await Task.Delay(1000);
}.NetネイティブのTaskは毎回新しいオブジェクトを作成して返しますが、UnityでラップされたAwaitableオブジェクトは共有されるため、GCの負荷が比較的小さくなります。Awaitableには、スレッド切り替えが「無料」(無損失)であるという利点もあります。詳細は次のセクションを参照してください。
async Awaitable function() {
while(true) {
await Awaitable.NextFrameAsync();
}
}繰り返しawaitする必要があるメソッド、上記のサンプルコードのような場合には、Awaitableの戻り値を使用してください。もちろん、IOのような一回きりのawaitを行うメソッドは問題ありません。
Awaitableオブジェクトは共有されているため(await後に他の人に使用されます)、繰り返しawaitすることはできません(以下のコードのように)。デッドロックを引き起こします。
async Awaitable function() {
while(true) {
var returned_awaitable = Awaitable.NextFrameAsync();
await returned_awaitable;
await returned_awaitable; //deadlock
}
}Unity内部でのAsyncのコンテキスト(切り替え)管理は、.Netと異なる
Asyncの主要な概念である、再開Resumeとは、切り替えて戻り、await後のコードの実行を続けることを指します。
「コンテキスト」は、どのようにawaitしたコードを再開するかを決定します。.Netはスレッドプールを使用して、異なる非同期コード間で切り替えます。例えば、以下の.Netコード:
using System;
async Task function() {
Console.WriteLine($"task start: Tid {Environment.CurrentManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"task end: Tid {Environment.CurrentManagedThreadId}");
}
Console.WriteLine($"caller start, Tid{Environment.CurrentManagedThreadId}");
await function();
Console.WriteLine($"caller end, Tid{Environment.CurrentManagedThreadId}");実行結果は以下の通りです:
caller start, Tid12
task start: Tid 12
task end: Tid 38
caller end, Tid38呼び出し元と実行元の両方でスレッド切り替えが発生していることがわかります。
一方、Unityはメインスレッドから呼び出されたメソッドがメインスレッド内でのみ実行されることを保証するために、UnitySynchronizationContextという独自のコンテキストメソッドをラップしています(これは内部実装であり、私たちは知っているだけでよく、気にする必要はありません)。
したがって、スレッドの問題を心配する必要はありません。戻り値がAwaitableであろうと.NetのTaskであろうと、メインスレッドから呼び出されたものであれば、すべてUnitySynchronizationContextによって管理され、メインスレッド上で実行されることが保証されます。
ただし、Taskを戻り値とするAsync関数は、再開Resume時に、即座にではなく次のフレームでしか再開できず、またUnityは.Netコンテキストを置き換える操作を1つ余分に実行する必要があるため、Awaitableの方が性能が高くなります。
もちろん、他のスレッドで呼び出されるAsync関数にはメインスレッド制限の要件がないため、.Netのスレッドプールが引き続き使用されます。
使用パターン
発射後放置(Fire-and-Forget)パターン
Async関数を呼び出す際にawaitを使用せず、システム内部でそのAsync関数を継続的に実行させることができます。
async Awaitable worker() {
await Awaitable.WaitForSecondsAsync(5);
Debug.Log($"worker end, frame {Time.frameCount}");
}
void caller() {
_ = worker();
Debug.Log($"caller end, frame {Time.frameCount}");
}結果:
caller end, frame 1
worker end, frame 292callerはawaitを書いていませんが、workerは依然として実行され、ただその完了を待たないだけです。
.Netでは、workerがスレッドプールで継続的に実行されますが、Unityではメインスレッドのループによって継続的に実行されます。
このパターンは、通常のSync同期関数内でAsync非同期関数を呼び出す際にもよく使用されます。
注意:この方法はasync voidと同等であり、最上位(スタックが失われる)で実行されるため、コード内の例外は見えず、コードが実行されなかったかのように見えます。このパターンが使えないわけではありません。
UnityではAsyncを使用してマルチスレッドプログラミングを簡単に実現できる
これを使用すると、Jobなどを必要とせずに、マルチスレッドコードを簡単に実装できます。任意のコード片を異なるスレッドで実行させることができます。
バックグラウンドスレッドプール(.Netスレッドプール実装に類似)に切り替えたい場合、またはメインスレッドに切り替えたい場合は、手動で指定できます。例えば:
async Awaitable function() {
await Awaitable.BackgroundThreadAsync();
...
return null;
}このawait行以降のコードは、関数が終了するまで、バックグラウンドの.Netマネージドスレッドプールで実行されます。
呼び出し元がメインスレッドの場合、切り替えは呼び出し元のスレッドを変更しないため、非常に便利です。切り替えには少なくとも1フレームの時間がかかることに注意してください。
メインスレッドへの切り替えはawait Awaitable.MainThreadAsync();です。通常は呼び出す必要はありません。
しかし残念ながら、WebGL(wasm)環境では、.Netマルチスレッド、およびTaskを使用するメソッドやAwaitable.BackgroundThreadAsync()を含む、あらゆるマルチスレッド機能を使用することはできません。
Async関数をvoidとして定義し、様々なeventコールバックに対応可能
コールバックでAsync関数を実行したい場合、非同期関数を呼び出すための普通の関数をラップする必要はなく、async voidを使用できます。以下のような感じです:
async void function(CallbackContext ctx) {
try{
...何か処理...
await Awaitable.WaitForSecondsAsync(5);
}catch (Exception e){
Debug.LogError(e);
}
}
void Start() {
InputAction.performed += function;
}async voidは最上位で実行されるため、関数内の例外は外部から検出されず、自分でcatchして処理する必要があることに注意してください。
MonoBehaviourイベントの非同期形式
MonoBehaviourのStart、Updateなどは、asyncモードで使用できます。例:async Awaitable Start() {}。ただし、非同期のStartやAwakeが実行完了する前に、Updateが呼び出されることに注意してください。
55a10c6