UniTask 実装で意識している 7 つのポイント
Unity で非同期的な処理を書くとき、多くの人はまず コルーチン(Coroutine) を使うと思います。
コルーチンは簡単に書けて便利ですが、
複数の非同期処理を組み合わせたり、戻り値を扱ったりすると、コードが複雑になりがちです。
そこで登場するのが UniTask です。
UniTask は、Unity 向けに最適化された軽量な非同期処理用ライブラリで、
コルーチンよりもパフォーマンス面で優れており、
戻り値を返すメソッドも async/await 構文を使ってより自然に書けます。
とても便利な UniTask ですが、特に キャンセル処理周り は注意が必要です。
ここでは、UniTask を使う際に気をつけたいポイントをまとめます。
✅ 1. CancellationTokenSource の Dispose を忘れないこと
CancellationTokenSource は IDisposable を実装しています。
使い終わったら必ず Dispose する ようにしましょう。
using (var cts = new CancellationTokenSource())
{
await SomeTaskAsync(cts.Token);
} // using で自動的に Dispose
忘れるとメモリリークの原因になります。
✅ 2. WhenAny のキャンセルを忘れないこと
UniTask.WhenAny は複数のタスクのうち、最初に完了したものを待機できる便利な関数です。
ただし、完了しなかった方のタスクはそのまま動き続ける 点に注意が必要です。
using var cts = new CancellationTokenSource();
var token = cts.Token;
var index = await UniTask.WhenAny(HogeAsync(token), FugaAsync(token));
// ここで忘れずに不要なタスクをキャンセルする
cts.Cancel();
// index に応じた処理をする
いずれかが終わったら、残りはキャンセル!
放置するとゾンビタスクが残り続けます。
✅ 3. Forget に渡す CancellationToken には注意すること
ローカルで作った CancellationTokenSource の Token を Forget に渡しても、
その CTS はスコープを抜けるときに破棄されるため意味のないものになってしまいます。
そのため、メンバー変数などで CTS を保持する必要性が出てきます。
仮に Cancel 不要なら CancellationToken.None を渡す、もしくは何も渡さないのも手です。
❌ 悪い例:ローカル CTS を渡す
void Start()
{
using var cts = new CancellationTokenSource();
SomeTaskAsync(cts.Token).Forget();
}
⭕ メンバー CTS を使う
private CancellationTokenSource _cts;
void Start()
{
_cts = new CancellationTokenSource();
SomeTaskAsync(_cts.Token).Forget();
}
⭕ Cancel 不要なら None を明示
SomeTaskAsync(CancellationToken.None).Forget();
✅ 4. MonoBehaviour の寿命と CancellationToken をリンクさせること
コルーチンとは異なり MonoBehaviour が破棄されてもタスクが動き続ける ケースがあります。
基本的には destroyCancellationToken を使えばよいのですが、
public 関数で外部からも CancellationToken が渡ってきている時には以下のようにトークンをリンクさせる必要があります。
public async UniTask DoSomethingAsync(CancellationToken cancellationToken)
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this.destroyCancellationToken);
await SomeTaskAsync(linkedCts.Token);
...
}
ただし、ここでもうひとつ重要な注意点があります。
GameObject が非アクティブな状態で Destroy() された場合 OnDestroy が呼ばれないため、destroyCancellationToken はキャンセルされません。
また、注意しなければいけないのは MonoBehaviour に限った話ではありません。
MonoBehaviour では問題が顕在化しやすいため特に注意が必要ですが、原理としてはインスタンス寿命と CancellationToken の管理のズレに起因するため、ピュア C# のクラスでも注意が必要です。
✅ 5. OperationCanceledException は catch しなくても大丈夫
CancellationTokenSource のキャンセル時に OperationCanceledException が発生しますが、
OperationCanceledException は他の NullReferenceException などの例外とは違い、
特に気にかけなくてもアプリケーションがクラッシュするようなものではありません。
try
{
...
await UniTask.WaitForSeconds(duration, cancellationToken: token);
...
}
catch (OperationCanceledException)
{
// catch しなくてもクラッシュしないので、この catch は無くても大丈夫
}
try ~ catch を書かずにキャンセルされたとしても、それ以降の処理が実行されないだけです。
ちょうど StopCoroutine() した時と同じような挙動になります。
特に例外処理を必要としないのであれば
try ~ catch を書かないほうがスッキリして読みやすいと感じます。
✅ 6. キャンセルとスキップを区別すること
キャンセルとスキップは似ていますが、意味がまったく異なります。
たとえば、会話メッセージのタイプライター表示を考えてみましょう。
- ボタンを押して 全文を一気に表示する のは「スキップ」
- ボタンを押して 表示を途中でやめる のは「キャンセル」
どちらも「途中で処理を止めるように見える」という点では似ていますが、
はじめに決めておかないと混乱のもとになります。
キャンセルは処理を中断するために CancellationToken で管理します。
一方、スキップは処理自体は続くものの、演出を飛ばすだけです。
そのため、スキップしたい場合は専用のフラグで制御するのが安全です。
でも、イベントシーンやムービーシーンをスキップする際に、
フェードアウト後にムービー再生処理に対して下す命令はキャンセルなんじゃないのかとか・・・w
とにかく、何がキャンセルで何がスキップなのか明確に決めておかないと混乱します。
これは Task の仕様というよりも、プログラムの設計上の考え方 に関わる部分です。
✅ 7. 不要な await を避けること(GC Alloc 対策)
async 関数にして await を使うと、わずかですが GC Alloc が発生します。
// よくない例
private async UniTask FooAsync()
{
await BarAsync();
}
このようなケースでは、async を外して
単にタスクを返すだけにするのが最適です。
// よい例
private UniTask FooAsync()
{
return BarAsync();
}
これだけで GC Alloc を抑えられます。
「await が最後の 1 行だけ」という関数は return task; にできないか確認する癖をつけましょう。
これは次のようなコードで確認できます。
public class Test : MonoBehaviour
{
void Start()
{
RunAllAsync().Forget();
}
private async UniTaskVoid RunAllAsync()
{
var tasks = new List<UniTask>();
for (int i = 0; i < 10000; i++)
{
var hoge = new Hoge();
tasks.Add(hoge.ProcessAsync());
}
await UniTask.WhenAll(tasks);
}
private class Hoge
{
public async UniTask ProcessAsync()
{
await UniTask.WaitForSeconds(1);
}
}
}

ProcessAsync の async をやめて return UniTask.WaitForSeconds(1); に変更すると
AwaiterActions.Continuation() の呼び出し回数が減ります。
おわりに
UniTask はコルーチンよりも柔軟で表現力の高い非同期処理が書ける、とても便利なライブラリです。
そのぶん、CancellationToken の扱いやタスクの寿命管理など、気を付けるべき点も多いと感じます。
私自身もまだまだ UniTask とキャンセルを完全に理解しきれているわけではありません。
細かいことを含めると注意点は7つではないですし、
無意識でたまたま罠を回避していたり、罠に気づかないまま進めていることなどもあるかと思います。
キャンセル周りは難しいですね。
Discussion