前提
- Unity 2018.4.11f1
- この記事は、個人的な探求の過程を記録したものです。内容は、記録時点での私の理解であり、必ずしも真実ではありません。
研究の範囲
- Async Awaitとは何なのか?
- できること、できないこと。
- コルーチンの代替がどこまでできるか?
#ドキュメント
- async (Microsoft C# リファレンス)
- Task Class (Microsoft .NET Framework)
基礎的なこと (執筆開始時点の理解)
- namespace
using System.Threading.Tasks;
を使用します。 -
Aysync
(エイシンク)、Await
(アウェイト)みたいな読み方です。 -
Task
を返す非同期メソッドは、IEnumerator
を返すメソッド(コルーチン)みたいに、呼び出しがブロックされません。- 呼び出し時点でメソッドの処理が完了していなくても呼び出し側に制御が戻ります。
-
IEnumerator
のように「必ず」ではなく、即座に完了して戻る場合もあるようです。 - メソッドの呼び出しに
await
を付けると、メソッドの完了まで待つ(同期する)ようになります。- コルーチンを
Update
中のMoveNext
で回したみたいな感じでしょうか。
- コルーチンを
-
Task<T>
を返すメソッドに付けられたawait
の型は、Task
が外れてT
になります。
-
Await
を使いたければ、メソッドの宣言にAsync
を付けてTask
かTask<T>
を返さなければなりません。 -
Async
を付したメソッドは、一般に、何も返さないかTask型を返すようにします。-
void
だとawait
を付けられません。そこはStartCoroutine
と同じような感じですね。 -
Task
ならawait
が付けられるけど、値を返せません。
-
-
await Task.Delay (int millisec)
で、yield return new WaitForSeconds (float sec)
のようなことができます。- あくまでも
float
で指定したい場合は、await Task.Delay (TimeSpan.FromSeconds (float sec))
のようにします。
- あくまでも
-
Task.Run ()
を使うと、別スレッドが走るので、Task.Run ()
の先では、Unityの一部の機能は使えなくなります。-
Task.Factory
を使うのは古いやり方です。(.NET Framework 4.xが使えない場合)
-
試行と理解
デッドロック
Task<int> task;
void Start () {
task = TestAsync ();
}
void Update () {
Debug.Log (task.Result);
}
async Task<int> TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
Debug.Log ("end task");
return 1;
}
Task
のプロパティにResult
を見つけて、安易に上記のようなことをしたらエディターが応答しなくなりました。
void Start () {
var task = TestAsync ();
Debug.Log (task.Result);
}
async Task<int> TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
Debug.Log ("end task");
return 1;
}
こう書いても同じです。
ドキュメントをきちんと読むと、Result
は、メソッドの処理が完了して結果が出るまでブロックされるようです。
つまり、以下のように書いても同じようにデッドロックが生じるのでしょうか。
やってみると、やはりエディタが応答しなくなります。
void Start () {
var task = TestAsync ();
task.Wait ();
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
Debug.Log ("end task");
}
これって、await
とどう違うの?
以下のように、await
を使って書けば、デッドロックは生じません。
async void Start () {
var task = TestAsync ();
await task;
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
Debug.Log ("end task");
}
await
とWait ()
はどう違うのでしょうか?
無限に待つWait ()
の代わりに、指定時間だけ待つWait (int millisec)
ようにして、変化を探ります。
void Start () {
Debug.Log ("start");
var task = TestAsync ();
task.Wait (2000); // 2秒間taskの完了を待つ
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
Debug.Log ("end task");
}
この結果は以下のようになります。
start
begin task
~ 2秒 ~
done
end task
次に、内外の待ち時間を逆にして試します。
void Start () {
Debug.Log ("start");
var task = TestAsync ();
task.Wait (1000); // 1秒間taskの完了を待つ
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (2000); // 2秒待機
Debug.Log ("end task");
}
この結果は以下のようになります。
start
begin task
~ 1秒 ~
done
~ 1秒 ~
end task
これらの試行から、次のようなことが解ると思います。
- 長い方の待ち時間を超えないことから、双方の待機時間が平行して経過していること。(
Task.Delay
は別スレッドみたいですね) -
done
がend task
より先であることから、メイン側が待つことをやめて制御を返すまで、メソッド側が完了できないこと。
以上によって、以下のような理解を得ることができました。
-
await
は、以降の処理をコルーチン終了時にコールバックで処理するように登録して、自身を終えて制御を返すようなもの (※) -
Wait ()
は、コルーチンを呼んだ後で、コルーチン終了待ちのループをするようなもの?
void Start () {
StartCoroutine (TestAsync (() => Debug.Log ("done")));
}
IEnumerator TestAsync (Action onCompleted) {
Debug.Log ("begin task");
yield return new WaitForSeconds (1f); // 1秒待機
Debug.Log ("end task");
onCompleted ();
}
こう考えると、時間指定なしのWait ()
の使いどころが思いつかないですね。
…もしかして、終了待ちのループでコルーチンが止まらなければ使える?
以下のように別スレッドで走らせれば、デッドロックしないのでしょうか。
void Start () {
Debug.Log ("start");
var task = Task.Run (() => TestAsync ()); // 別スレッドで実行
task.Wait ();
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
Debug.Log ("end task");
}
確かにそのようです。
以下の結果が得られました。
start
begin task
~ 1秒 ~
end task
done
ただし、メインルーチンが最初から最後まで制御を戻さないので、ログは全て一息に表示されます。
あるいは、これでもいけるようです。
void Start () {
Debug.Log ("start");
TestAsync ().Wait ();
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000).ConfigureAwait (false); // 1秒待機
Debug.Log ("end task");
}
通常のawait
は、.ConfigureAwait (true)
になっているようです。
ドキュメントを読むと、true
だと、続きの処理を大元の環境で実行しようとして、false
だと環境を戻さないということらしいです。
元の環境に戻りたくても、元の環境では終了待ちループ(Wait
)が居座って制御を離さない、非同期処理が終わらないからWait
が終わらない、斯くしてデッドロックが生じるわけですね。
この環境というのはスレッドなのでしょうか?
だとしたら、明示的に指示しなくても、別スレッドで実行される可能性があるということになりますが…。
using System.Threading;
void Start () {
Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
TestAsync ().Wait ();
Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}
async Task TestAsync () {
Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay (1000).ConfigureAwait (false);
Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
begin task 1
~ 1秒 ~
end task 156
done 1
確かに、別スレッドで実行されたようです。
別スレッドを明示した場合と比較してみましょう。
void Start () {
Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
var task = Task.Run (() => TestAsync ()); // 別スレッドで実行
Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}
async Task TestAsync () {
Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay (1000); // 1秒待機
Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
done 1
begin task 189
~ 1秒 ~
end task 184
おや?
.ConfigureAwait (false)
していないのに、await
の前後が別スレッドになっていますね。
async
とawait
だけならどうでしょうか。
void Start () {
Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
var task = TestAsync ();
Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}
async Task TestAsync () {
Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay (1000); // 1秒待機
Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
begin task 1
done 1
~ 1秒 ~
end task 1
-
async
とawait
だけなら、シングルスレッドで動作していますが、Task.Run ()
はもちろん、.ConfigureAwait (false)
を使った場合も、別スレッドで実行されるようです。 - 別スレッドでも、
Debug.Log
は使えるようですね。
例外の伝播
Task.Run ()
を使わずに、async
とawait
だけを使う分には、Task
クラスが完了待ちループawait
の中で例外を伝達してくれるようです。
async void Start () {
Debug.Log ("start");
var task = TestAsync ();
await task; // 完了待機
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
この結果は以下のようになります。
start
begin task
~ 1秒 ~
Exception
しかし、メイン側で完了待ちループawait
しないと、例外が受け取れません。
async void Start () {
Debug.Log ("start");
var task = TestAsync ();
// await task; // 完了待機
Debug.Log ("done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
この結果は以下のようになります。
start
begin task
done
Task
でなくvoid
の場合も、もちろん、同じ結果です。
と、思いきや、違ってました。
async void Start () {
Debug.Log ("start");
/*var task = */TestAsync ();
// await task; // 完了待機
Debug.Log ("done");
}
async void TestAsync () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
start
begin task
done
~ 1秒 ~
Exception
このように、void
の場合は例外が受け取れます。
どうやら、Task
を受け取った以上はawait
しなさいと言うことでしょうか。
では、Task.Run ()
の場合はどうでしょうか。
void Start () {
Debug.Log ("start");
var task = Task.Run (() => Test2Async ());
await task;
Debug.Log ("done");
}
async Task Test2Async () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
Task.Run ()
をawait
すれば受け取れるようです。
しかし、並列に動かそうとしてawait
を外すと…
void Start () {
Debug.Log ("start");
var task = Task.Run (() => Test2Async ());
//await task;
Debug.Log ("done");
}
async Task Test2Async () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
これだと、例外は受け取れません。
こちらは、void
にしてもだめなようです。
以下のようにしても同じです。
void Start () {
Debug.Log ("start");
Task.Run (async () => await Test2Async ());
Debug.Log ("done");
}
async Task Test2Async () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
でも、以下のようにすれば、ブロックされずに受け取れるようです。
void Start () {
Debug.Log ("start");
TestAsync ();
Debug.Log ("done");
}
async void TestAsync () {
var task = Task.Run (() => Test2Async ());
await task;
}
async Task Test2Async () {
Debug.Log ("begin task");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ("end task");
}
start
done
begin task
~ 1秒 ~
Exception
間接的にawait Task.Run ()
を使うことで並列に処理しつつ、await
がメインスレッドで処理されるので、例外を受け取ることもできるということでしょうか。
じゃあ、これはどうでしょう?
void Start () {
Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
TestAsync ();
Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}
async void TestAsync () {
Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay (1000).ConfigureAwait (false); // 1秒待機
Debug.Log ($"continue task {Thread.CurrentThread.ManagedThreadId}");
throw new Exception (); // 例外発生
Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
begin task 1
done 1
~ 1秒 ~
continue task 84
Exception
あれ?
別スレッドになるから、てっきりダメかと思ったけど、ちゃんと受け取れますね。
う~ん???
受け方\戻り値 | void | Task |
---|---|---|
◯ | ✕ | |
await | - | ◯ |
Task.Run | ✕ | ✕ |
await Task.Run | ✕ | ◯ |
マルチスレッド
シングルスレッドでマルチタスク
まず、複数の非同期メソッドをawait
なしに呼び出して、ループさせてみます。
void Start () {
Debug.Log ("start");
TestAsync (1);
TestAsync (2);
Debug.Log ("done");
}
async void TestAsync (int number) {
var task = Test2Async (number);
await task;
}
async Task Test2Async (int number) {
Debug.Log ($"begin task {number}");
for (var i = 0; i < 1000; i++) {
Debug.Log ($"continue task {number} {i}");
}
Debug.Log ($"end task {number}");
}
シングルスレッドで実行されるので、一方のループが終わってから他方のループが始まります。
start
done
begin task 1
begin task 2
end task 1
done
continue task 1 0~999
end task 1
continue task 2 0~999
end task 2
マルチスレッドでマルチタスク
同じことを、複数のTask.Run ()
により別スレッドで走らせてみます。
void Start () {
Debug.Log ("start");
TestAsync (1);
TestAsync (2);
Debug.Log ("done");
}
async void TestAsync (int number) {
var task = Task.Run (async () => await Test2Async (number));
await task;
}
async Task Test2Async (int number) {
Debug.Log ($"begin task {number}");
for (var i = 0; i < 1000; i++) {
Debug.Log ($"continue task {number} {i}");
}
Debug.Log ($"end task {number}");
}
この場合は、ふたつのカウントが同時に進みます。
なお、task 1と2の処理順は、不確定になるようです。
また、エディタで実行を終了しても、カウントは継続します。
(カウント中にコンパイルが入るとエディタがフリーズします。)
別スレッドでの例外
例外を発生させてみます。
void Start () {
Debug.Log ("start");
TestAsync (1);
TestAsync (2);
Debug.Log ("done");
}
async void TestAsync (int number) {
var task = Task.Run (async () => await Test2Async (number));
await task;
}
async Task Test2Async (int number) {
Debug.Log ($"begin task {number}");
await Task.Delay (1000); // 1秒待機
throw new Exception (); // 例外発生
Debug.Log ($"end task {number}");
}
この結果は、以下のように例外を2回受け取ります。
メインスレッドでawait
すれば、別スレッドの例外も受け取れるようですね。
start
done
begin task 1
begin task 2
~ 1秒 ~
Exception
Exception
yield return new WaitWhile (Func<bool> predicate)
相当するものは見つけられていません。
普通にループを書いてしまうと、そのスレッドをブロックしてしまいますよね。
スレッドを解放しつつ、時々ポーリングするような…。
ああ、そういうことですね。
void Start () {
Debug.Log ($"start");
var task = TestAsync ();
Debug.Log ($"done");
}
async Task TestAsync () {
Debug.Log ("begin task");
await WaitWhileAsync (() => UnityEngine.Random.Range (0, 10) != 0); // 賽の目が出るまで待機
Debug.Log ("end task");
}
async Task WaitWhileAsync (Func<bool> predicate) {
while (predicate ()) {
await Task.Delay (33); // 1/30秒弱待機
}
}
これでいけそうです。
直に書いても知れていますね。
StopCoroutine (coroutine)
タスクの停止は、コルーチンのような外部からの強制停止ではなくて、外部からの要求を内部で監視して、都合の良いタイミングで自ら終わるという感じです。
async void Start () {
Debug.Log ($"start");
var tokenSource = new CancellationTokenSource ();
var token = tokenSource.Token; // 中断トークン
var task = Task.Run (async () => await TestAsync (token), token);
await Task.Delay (1000); // 1秒待つ
tokenSource.Cancel (); // キャンセル
Debug.Log ($"done");
}
async Task TestAsync (CancellationToken token) {
Debug.Log ("begin task");
for (var i = 0; i < 100; i++) {
if (token.IsCancellationRequested) { // キャンセルされた
Debug.Log ("canceled");
return;
}
await Task.Delay (16); // 1/60秒弱待機
}
Debug.Log ("end task");
}
start
begin task
~ 1秒 ~
done
canceled
この例のようにポーリングするのでなく、トークンに対してコールバックを設定することもできます。
応用
簡単リモートアセット(Unity Addressable Asset System)を試してみた (その1) (Qiita) のコルーチンをタスクに置き換えると、例えば以下のようになります。
private void Start () {
loader ();
}
private async void loader () {
// ロード
var textPrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/BottomText.prefab").Task;
var imagePrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/FullScreenImage.prefab").Task;
var spriteAssets = await Addressables.LoadAssetsAsync<Sprite> ("Sprites", null).Task; // ラベルを指定して一括ロード
// エラーがないことを確認
if (textPrefab && imagePrefab && spriteAssets != null && spriteAssets.Count > 0) {
// プレファブからオブジェクトを生成
var image = Instantiate (imagePrefab, transform).GetComponent<Image> ();
var text = Instantiate (textPrefab, transform).GetComponent<Text> ();
// スプライトを順に切り替え
for (var i = 0; i < spriteAssets.Count; i = (i + 1) % spriteAssets.Count) {
image.sprite = spriteAssets [i];
text.text = $"{spriteAssets [i].name} <size=20>© UTJ/UCL</size>";
await Task.Delay (3000);
}
}
}
これは問題なく動作します。
ここで、スライドショウが一巡するのを待って次の処理がしたいとして、以下のように書き直したものとします。
private void Start () {
loader ().Wait ();
// 開始後の処理
}
private async Task loader () {
// ロード
var textPrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/BottomText.prefab").Task.ConfigureAwait (false);
var imagePrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/FullScreenImage.prefab").Task.ConfigureAwait (false);
var spriteAssets = await Addressables.LoadAssetsAsync<Sprite> ("Sprites", null).Task.ConfigureAwait (false); // ラベルを指定して一括ロード
// エラーがないことを確認
if (textPrefab && imagePrefab && spriteAssets != null && spriteAssets.Count > 0) {
// プレファブからオブジェクトを生成
var image = Instantiate (imagePrefab, transform).GetComponent<Image> ();
var text = Instantiate (textPrefab, transform).GetComponent<Text> ();
// スプライトを順に切り替え
for (var i = 0; i < spriteAssets.Count; i++) {
image.sprite = spriteAssets [i];
text.text = $"{spriteAssets [i].name} <size=20>© UTJ/UCL</size>";
await Task.Delay (3000);
}
}
}
具体的には、loader
に対して、.Wait ()
できるようにTask
を返すようにして、デッドロックしないように、await
毎に.ConfigureAwait (false)
を付けます。
さらに、スライドのループが一巡で終わるようにしています。
しかし、コンパイルは通りますが、期待した動作はしません。
一番の問題は、Unityの機能であるAddressables.LoadAssetAsync
はメインスレッドでしか動作しないということです。
最初のawait Addressables.LoadAssetAsync~.ConfigureAwait (false)
によって、以降の処理が別スレッドに飛ばされます。
その結果、次のawait Addressables.LoadAssetAsync~
でエラーが生じます。
以下のようなことならできますが、同期的に処理したい場合は難しいですね。
private async void Start () {
await loader ();
// 次の処理
}
private async Task loader () {
// ~
コルーチンの場合でも、同様に以下のように書くことはできても、他の処理をブロックして待つのは難しいですね。
private IEnumerator Start () {
yield return StartCoroutine (loader ());
// 次の処理
}
Task拡張クラス
/// <summary>タスク拡張</summary>
public static class TaskEx {
/// <summary>休止間隔</summary>
private const int Tick = 16;
/// <summary>1フレーム待機</summary>
public static Task DelayOneFrame => Task.Delay (Tick);
/// <summary>条件が成立する間待機</summary>
/// <param name="predicate">条件</param>
/// <param name="limit">msec単位の制限</param>
/// <param name="tick">刻み</param>
public static async Task DelayWhile (Func<bool> predicate, int limit = 0, int tick = 0) {
tick = (tick > 0) ? tick : Tick;
if (limit <= 0) {
while (predicate ()) {
await Task.Delay (tick);
}
} else {
limit /= tick;
while (predicate () && limit-- > 0) {
await Task.Delay (tick);
}
}
}
/// <summary>条件が成立するまで待機</summary>
/// <param name="predicate">条件</param>
/// <param name="limit">msec単位の制限</param>
/// <param name="tick">刻み</param>
public static async Task DelayUntil (Func<bool> predicate, int limit = 0, int tick = 0) {
tick = (tick > 0) ? tick : Tick;
if (limit <= 0) {
while (!predicate ()) {
await Task.Delay (tick);
}
} else {
limit /= tick;
while (!predicate () && limit-- > 0) {
await Task.Delay (tick);
}
}
}
}
yield return null;
⇒ await TaskEx.DelayOneFrame;
yield return new WaitUntil (() => flag);
⇒ await TaskEx.DelayUntil (() => flag);
yield return new WaitWhile (() => flag);
⇒ await TaskEx.DelayWhile (() => flag);
とりあえず
今回はここまでにします。いろいろなことが解りました。
行きつ戻りつ書いたのと、きちんと見直せていないので、信頼性は低いです。
また、解らないことが出てきたら、手直ししたり、続きを書くかも知れません。
おさらいとまとめ
- namespace
using System.Threading.Tasks;
を使用します。 -
Aysync
(エイシンク)、Await
(アウェイト)みたいな読み方です。 -
Task
を返す非同期メソッドは、IEnumerator
を返すメソッド(コルーチン)みたいに、呼び出しがブロックされません。- 呼び出し時点でメソッドの処理が完了していなくても呼び出し側に制御が戻ります。
-
IEnumerator
のように「必ず」ではなく、即座に完了して戻る場合もあるようです。 - メソッドの呼び出しに
await
を付けると、メソッドの完了まで待つ(同期する)ようになります。-
await
は、以降の処理をくくっておいて、コルーチンの終了時にコールバックされるように登録し、自身は終了して制御を返します。 -
Wait ()
は、終了待ちループです。終了せず制御を返しません。
-
-
Task<T>
を返すメソッドに付けられたawait
の型は、Task
が外れてT
になります。
-
Await
を使いたければ、メソッドの宣言にAsync
を付けてTask
かTask<T>
を返さなければなりません。 -
Async
を付したメソッドは、一般に、何も返さないかTask型を返すようにします。-
void
だとawait
を付けられません。そこはStartCoroutine
と同じような感じですね。 -
Task
ならawait
が付けられるけど、値を返せません。
-
-
await Task.Delay (int millisec)
で、yield return new WaitForSeconds (float sec)
のようなことができます。- あくまでも
float
で指定したい場合は、await Task.Delay (TimeSpan.FromSeconds (float sec))
のようにします。
- あくまでも
-
Task.Run ()
を使うと、別スレッドが走るので、Task.Run ()
の先では、Unityの一部の機能は使えなくなります。-
Task.Factory
を使うのは古いやり方です。(.NET Framework 4.xが使えない場合)
-
-
async
とawait
だけなら、シングルスレッドで動作していますが、Task.Run ()
はもちろん、.ConfigureAwait (false)
を使った場合も、別スレッドで実行されるようです。 - 別スレッドでも、
Debug.Log
は使えるようです。 - 例外は、
await
越しに届きます。await
を使わない場合は届きません。
蛇足
並列と並行
- 並列(parallel): スレッド
- マルチスレッドで真に多重
- .NETのTPL
- 並行(concurrent): コルーチン
- シングルスレッドで時分割多重
- Unityの
StartCoroutine()
参考
試行の途中で拝読した下の記事は大変分かり易く参考になりました。
【図解】C#のasync/awaitの内部挙動を理解する (Qiita)
この記事には出てきませんが、IProgress<T>
を使った別スレッドの進捗をメインスレッドに伝達する仕組みについては、こちらを参考にさせていただきました。
Progressを使ってみた (ひょうろくだまらん)
どうもありがとうございました。