渋谷ほととぎす通信

新しいこと、枯れたこと問わず大庭が興味を持ったものを調査、生活の効率を求める完全趣味の技術ブログ。基礎を大事にしています。

async/awaitを使う上でタスクのキャンセルは例外処理であるという考え方に移行する必要がある件


この辺りの経緯から基礎的なところからUnityを使う上でのasync/awaitを学んでみます。

コルーチンと違って、戻り値を取得できる、コールバックが不要になるなど、便利だ便利だと周りが誘惑してきますが、個人的にはキャンセル処理が面倒臭いという点が一番気になっています。


複雑なシステムほどキャンセル処理は重要ですし、バグの温床になります。本記事だけでは終わらないと思いますが、キャンセル周りはしっかりと検証していこうと思っています。


本記事ではシンプルなタスクの実行とそのキャンセルについて調査しています。

Task.DelayでN秒待機する

async/awaitを学ぶ上でのテストコードとして、Task.Delayを使ったタスクを使っていきます。

実行すると5秒待機して Complete というログが出力されるショートコードです。

Task.Delayをキャンセルする

ボタンが押されたらタスクがキャンセルするという仕様で先ほどのTask.Delayをキャンセルさせます。

キャンセル実装するタスクに対して、 CancellationTokenSource を生成しTokenプロパティでトークンを発行し、そのトークンをDelayメソッドの第2引数に渡します。

実際にキャンセルを実行させるときは、 CancellationToken.Cancel() を実行することでキャンセルされます。

自前のタスクをキャンセルする

Task.DelayはCancellationTokenを引数に渡すだけでしたが、自前のカスタムタスクをキャンセルさせるためには別の処理が必要になります。

キャンセルをチェックするタイミングで CancellationToken.ThrowIfCancellationRequested() を実装します。

キャンセル処理は例外扱いなので注意

CancellationToken.Cancel()OperationCanceledException がスローするので、try-catchで例外をキャッチしておかないと以下のようなエラーが出力してプログラムが止まります。

TaskCanceledException: A task was canceled.
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.GetResult () (at <7d97106330684add86d080ecf65bfe69>:0)
AsyncAwaitTest+<ExecuteWait>d__3.MoveNext () (at Assets/AsyncAwaitTest.cs:37)
--- End of stack trace from previous location where exception was thrown ---
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.GetResult () (at <7d97106330684add86d080ecf65bfe69>:0)
AsyncAwaitTest+<Start>d__2.MoveNext () (at Assets/AsyncAwaitTest.cs:21)
--- End of stack trace from previous location where exception was thrown ---
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__6_0 (System.Object state) (at <7d97106330684add86d080ecf65bfe69>:0)
UnityEngine.UnitySynchronizationContext+WorkRequest.Invoke () (at /Users/builduser/buildslave/unity/build/Runtime/Export/Scripting/UnitySynchronizationContext.cs:115)
UnityEngine.UnitySynchronizationContext:ExecuteTasks()

タスク実行時には必ずtry-catchを使う

try
{
   // tryで囲ってタスクを実行する
    await ExecuteWait(_cts.Token);
}
catch (OperationCanceledException e)
{
    // タスクがキャンセルされた時の処理を書く
}

キャンセルするタスクは、上のような感じでtry-catchを使うことになり、OperationCanceledExceptionをキャッチしたらキャンセル処理を実装するということになります。

コルーチンとは180度考え方を変える必要あり

今までコルーチンでキャンセル処理を実装するときは、StopCoroutineを使う、または紐づくGameObjectを非アクティブする、または破棄するといった複数の手段が存在しました。


しかしasync/awaitは、CancellationTokenSourceのCancelメソッドを呼ぶ1択です。

今までUnityで体に染み付くほどコルーチンを使っていた身としては、


「タスクのキャンセルは例外処理である」


と、考え方を大きく変える必要があるなと思いました。