UniTask(Cysharp製)で非同期処理を行う際に欠かせない CancellationToken
の使い方を、生成方法・基本構文・応用・GC削減テクニック・ベストプラクティスまで、完全網羅で解説します。
🗂 目次
- 🗂 目次
- ✅ CancellationToken の生成・取得手段【完全リスト】
- 🔧 基本的な使い方一覧
- 🚀 応用的な使い方一覧
- 🧪 実践
- 🏆 ベストプラクティス
- 🛠 トラブルシューティング:CancellationToken 利用時のよくある問題と対策
- ❌ 1. CancellationTokenSource が使い回されていて、即キャンセルされる
- ❌ 2. CreateLinkedTokenSource() の使いすぎによる GC 発生
- ❌ 3. CancellationToken.None がキャンセルされると誤解している
- ❌ 4. UnsafeRegister() の解除忘れによるリーク
- ❌ 5. OperationCanceledException を補足せずクラッシュ
- ❌ 6. LinkedTokenSource を再利用して例外になる
- ❌ 7. MissingReferenceException: DestroyCancellation token should be called...
- ✅ デバッグ支援Tips
- 参考
✅ CancellationToken の生成・取得手段【完全リスト】
1. 明示的に生成する
1-1. 通常の CancellationTokenSource
var cts = new CancellationTokenSource(); var ct = cts.Token;
1-2. タイムアウト付き CancellationTokenSource
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var ct = cts.Token;
1-3. CancelAfterSlim
を使ったキャンセル予約
var cts = new CancellationTokenSource(); cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); var ct = cts.Token;
2. UniTask 独自の生成方法
2-1. TimeoutController
による生成
var tc = new TimeoutController(); var ct = tc.Timeout(TimeSpan.FromSeconds(5)); tc.Reset(); // 再利用可能にする
2-2. 外部 CancellationTokenSource
を渡して管理
var cts = new CancellationTokenSource(); var tc = new TimeoutController(cts);
3. Unity のライフサイクルと連動した生成
3-1. GameObject 破棄時
var ct = this.destroyCancellationToken; // Unity 2022.2+ var ct = this.GetCancellationTokenOnDestroy(); // 全バージョン対応
4. システムイベント起因の生成
4-1. アプリ終了時
var ct = Application.exitCancellationToken;
5. 複数トークンの合成生成
5-1. CancellationTokenSource.CreateLinkedTokenSource
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct1, ct2, ct3); var ct = linkedCts.Token;
6. 外部から受け取るトークン
6-1. メソッド引数で受け取る
private async UniTask ExecuteAsync(CancellationToken ctParam) { await SomeTask(ctParam); }
7. 特殊な固定トークン
7-1. CancellationToken.None
await UniTask.Delay(5000, cancellationToken: CancellationToken.None);
🔧 基本的な使い方一覧
項目 | 説明 |
---|---|
cts.Cancel() |
任意のタイミングでキャンセルを発行する。 |
cts.Token |
CancellationToken を取得して非同期処理に渡す。 |
ct.ThrowIfCancellationRequested() |
キャンセル済みなら即座に例外を投げる。 |
await UniTask.XYZ(..., cancellationToken: ct) |
非同期タスク中にトークンを渡し、キャンセル可能にする。 |
サンプルコード
cts.Cancel()
var cts = new CancellationTokenSource(); button.onClick.AddListener(() => cts.Cancel());
cts.Token
var cts = new CancellationTokenSource(); CancellationToken ct = cts.Token;
ct.ThrowIfCancellationRequested()
void DoWork(CancellationToken ct) { ct.ThrowIfCancellationRequested(); Debug.Log("キャンセルされていないので処理継続"); }
await UniTask.Delay(..., cancellationToken: ct)
var cts = new CancellationTokenSource(); try { await UniTask.Delay(5000, cancellationToken: cts.Token); } catch (OperationCanceledException) { Debug.Log("処理がキャンセルされました"); }
🚀 応用的な使い方一覧
応用技法 | 説明 |
---|---|
CreateLinkedTokenSource(...) |
複数トークンをまとめ、どれかがキャンセルされたら全体を中断。 |
TimeoutController.Reset() |
タイムアウトトークンを再利用するために呼び出す。 |
GetCancellationTokenOnDisable() |
GameObject が無効化されたときに自動キャンセル。 |
Application.exitCancellationToken |
アプリ終了時にキャンセルが発生。 |
SceneManager.activeSceneChanged + cts.Cancel() |
シーン変更時に手動キャンセル。 |
EditorApplication.quitting + cts.Cancel() |
Unity Editor 終了時にキャンセル処理。 |
CancellationToken.None |
キャンセル不可な処理。 |
CancellationToken.Register |
キャンセル発生時の処理を登録。 |
DOTS / TaskScheduler連携 | 外部スレッドやジョブにキャンセルを反映。 |
サンプルコード
CreateLinkedTokenSource(...)
var cts1 = new CancellationTokenSource(); var cts2 = new CancellationTokenSource(); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); await UniTask.Delay(10000, cancellationToken: linkedCts.Token);
TimeoutController.Reset()
var tc = new TimeoutController(); var ct = tc.Timeout(TimeSpan.FromSeconds(5)); try { await UniTask.Delay(10000, cancellationToken: ct); } catch (OperationCanceledException) { Debug.Log("タイムアウトによりキャンセルされました"); } tc.Reset();
GetCancellationTokenOnDisable()
await UniTask.Delay(3000, cancellationToken: this.GetCancellationTokenOnDisable());
Application.exitCancellationToken
await UniTask.Delay(5000, cancellationToken: Application.exitCancellationToken);
SceneManager.activeSceneChanged
var cts = new CancellationTokenSource(); SceneManager.activeSceneChanged += (oldScene, newScene) => { cts.Cancel(); }; await SomeAsyncTask(cts.Token);
EditorApplication.quitting
#if UNITY_EDITOR var cts = new CancellationTokenSource(); EditorApplication.quitting += () => cts.Cancel(); await UniTask.Delay(5000, cancellationToken: cts.Token); #endif
CancellationToken.None
await UniTask.Delay(3000, cancellationToken: CancellationToken.None);
CancellationToken.Register
var cts = new CancellationTokenSource(); cts.Token.Register(() => { // キャンセル発生時の処理 });
DOTS/Task連携(擬似例)
var cts = new CancellationTokenSource(); NativeArray<bool> cancelFlag = new NativeArray<bool>(1, Allocator.Persistent); JobHandle job = new CancelableJob { cancelFlag = cancelFlag }.Schedule(); UniTask.Void(async () => { await UniTask.Delay(3000, cancellationToken: cts.Token); cancelFlag[0] = true; });
🧪 実践
キャンセル処理とタイムアウトの組み合わせ
public async UniTask RunTaskWithFlexibleCancellation(CancellationToken externalToken) { var manualCts = new CancellationTokenSource(); var timeoutCts = new CancellationTokenSource(); timeoutCts.CancelAfterSlim(TimeSpan.FromSeconds(10)); var destroyToken = this.GetCancellationTokenOnDestroy(); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( manualCts.Token, timeoutCts.Token, destroyToken, externalToken ); try { await UniTask.Delay(30000, cancellationToken: linkedCts.Token); } catch (OperationCanceledException ex) { if (timeoutCts.IsCancellationRequested) Debug.Log("タイムアウトでキャンセル"); else if (destroyToken.IsCancellationRequested) Debug.Log("破棄でキャンセル"); else if (externalToken.IsCancellationRequested) Debug.Log("親トークンでキャンセル"); else Debug.Log("手動キャンセル"); } }
♻️ 再利用可能な設計で GC 削減(Stackベース)
static class CtsPool { private static readonly Stack<CancellationTokenSource> pool = new(); public static CancellationTokenSource Rent() => pool.Count > 0 ? pool.Pop() : new CancellationTokenSource(); public static void Return(CancellationTokenSource cts) { if (cts.IsCancellationRequested) cts.Dispose(); else { cts.Reset(); pool.Push(cts); } } }
🏆 ベストプラクティス
ObjectPool と UnsafeRegister によるゼロアロケーション設計
この設計は、UniTask の作者でもある Cysharp の開発者が解説している公式記事:
👉 CancellationTokenとゼロアロケーション(neue.cc, 2022年7月13日)
の解説です。
🎯 このコードの目的
CancellationTokenSource
をObjectPool
で再利用し GCを抑制UnsafeRegister
により 複数トークンを軽量に統合- 発生源に応じた 適切な例外の再スロー
- 明示的な Dispose管理とプール返却
📄 実装コード(整形済み)
class Client : IDisposable { readonly TimeSpan timeout; readonly ObjectPool<CancellationTokenSource> timeoutTokenSourcePool; readonly CancellationTokenSource clientLifetimeTokenSource; public TimeSpan Timeout { get; } public Client(TimeSpan timeout) { this.Timeout = timeout; this.timeoutTokenSourcePool = ObjectPool.Create<CancellationTokenSource>(); this.clientLifetimeTokenSource = new CancellationTokenSource(); } public async Task SendAsync(CancellationToken cancellationToken = default) { var timeoutTokenSource = timeoutTokenSourcePool.Get(); CancellationTokenRegistration externalCancellation = default; if (cancellationToken.CanBeCanceled) { externalCancellation = cancellationToken.UnsafeRegister(static state => { ((CancellationTokenSource)state!).Cancel(); }, timeoutTokenSource); } var clientLifetimeCancellation = clientLifetimeTokenSource.Token.UnsafeRegister(static state => { ((CancellationTokenSource)state!).Cancel(); }, timeoutTokenSource); timeoutTokenSource.CancelAfter(Timeout); try { await SendCoreAsync(timeoutTokenSource.Token); } catch (OperationCanceledException ex) when (ex.CancellationToken == timeoutTokenSource.Token) { if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException(ex.Message, ex, cancellationToken); } else if (clientLifetimeTokenSource.IsCancellationRequested) { throw new OperationCanceledException("Client is disposed.", ex, clientLifetimeTokenSource.Token); } else { throw new TimeoutException($"The request was canceled due to the configured Timeout of {Timeout.TotalSeconds} seconds elapsing.", ex); } } finally { externalCancellation.Dispose(); clientLifetimeCancellation.Dispose(); if (timeoutTokenSource.TryReset()) { timeoutTokenSourcePool.Return(timeoutTokenSource); } } } async Task SendCoreAsync(CancellationToken cancellationToken) { // 通信処理など } public void Dispose() { clientLifetimeTokenSource.Cancel(); clientLifetimeTokenSource.Dispose(); } }
🔍 処理解説(要点まとめ)
処理 | 解説 |
---|---|
ObjectPool.Get() / Return() |
再利用によってGC削減 |
UnsafeRegister(...) |
複数トークンのキャンセルを1つに束ねる高速API |
CancelAfter(...) |
タイムアウト機能 |
TryReset() |
再利用可否チェック(.NET 6+) |
Dispose() の明示管理 |
メモリリーク防止・登録解除保証 |
✅ メリット
⚠️ 注意点
TryReset()
は .NET 6 以上限定UnsafeRegister
を使う場合は 登録解除(Dispose)を必ず行う- Unityで使用する場合は、**
ObjectPool
を拡張導入(NuGetなど)**が必要 - UniTask 環境なら CancelAfterSlim へ差し替えると軽量化
🛠 トラブルシューティング:CancellationToken 利用時のよくある問題と対策
❌ 1. CancellationTokenSource
が使い回されていて、即キャンセルされる
原因:
cts.Cancel()
呼び出し後に CancellationTokenSource
をそのまま再利用している。
解決策:
if (cts.IsCancellationRequested) { cts.Dispose(); cts = new CancellationTokenSource(); }
❌ 2. CreateLinkedTokenSource()
の使いすぎによる GC 発生
原因:
頻繁に CreateLinkedTokenSource()
を呼び出すことで GC アロケーションが蓄積される。
解決策:
UnsafeRegister()
による手動連携で GC を抑制。- 再利用可能なキャンセル設計に切り替える。
❌ 3. CancellationToken.None
がキャンセルされると誤解している
原因:
CancellationToken.None
は「絶対にキャンセルされない固定トークン」であり、キャンセル機能は無い。
解決策:
中断可能な処理には必ず CancellationTokenSource.Token
を使用する。
❌ 4. UnsafeRegister()
の解除忘れによるリーク
原因:
UnsafeRegister()
で登録したキャンセルリスナーを .Dispose()
せずに放置。
解決策:
var reg = ct.UnsafeRegister(...); try { await SomeAsyncTask(ct); } finally { reg.Dispose(); }
❌ 5. OperationCanceledException
を補足せずクラッシュ
原因:
非同期処理中にキャンセルされたとき、例外補足をしておらず unhandled exception に。
解決策:
try { await SomeAsyncTask(ct); } catch (OperationCanceledException) { Debug.Log("キャンセルされました"); }
❌ 6. LinkedTokenSource
を再利用して例外になる
原因:
CreateLinkedTokenSource()
で作成したトークンは1回限りの使い切り用。再利用は未サポート。
解決策:
- 処理ごとに新規で生成し、終わったら
Dispose()
。 - 頻繁な再利用が必要な場合は
UnsafeRegister
方式へ移行。
❌ 7. MissingReferenceException: DestroyCancellation token should be called...
エラーメッセージ:
UnityEngine.MissingReferenceException: DestroyCancellation token should be called atleast once before destroying the monobehaviour object
原因:
非同期処理の中で、すでに破棄された MonoBehaviour
の destroyCancellationToken
を参照した。
再現コード(NGパターン):
await UniTask.Delay(1000); // この時点で this が破棄済み var ct = this.destroyCancellationToken; // → MissingReferenceException
解決策(OKパターン):
var ct = this.destroyCancellationToken; // 事前にキャッシュ await UniTask.Delay(1000, cancellationToken: ct);
補足と参考リンク:
destroyCancellationToken
は アクセス前に初期化が必要。- Unity 2022.2 以降で導入された仕様のため、古い環境では
GetCancellationTokenOnDestroy()
を使うと安全。 - 内部実装: 👉 MonoBehaviour.destroyCancellationToken の実装(Unity公式)
✅ デバッグ支援Tips
テクニック | 効果 |
---|---|
ct.IsCancellationRequested をログ出力 |
キャンセル発生のタイミング調査に有効 |
Debug.LogException(e) |
発生した例外のスタックトレースと原因を把握 |
.TryReset() で CTS 再利用可否を判定 |
GC抑制しつつ安全に再利用可能 |
このセクションは、UniTask + CancellationToken のトラブルを未然に防ぎ、再発を抑えるための実践的なチェックリストです。 特に「非同期処理+Unityのライフサイクル」はバグの温床となるため、上記の設計・パターンで安定性を確保することが重要です。
参考
- 【C#】async/awaitのキャンセル処理まとめ #Unity - Qiita
- 【Unity】コルーチンからUniTaskに乗り換えるときに気をつけるべきところ #Unity - Qiita
- UniTask 処理のタイムアウトの書き方 まとめ #C# - Qiita
- neue cc - C#のasync/await再考, タイムアウト処理のベストプラクティス, UniTask v2.2.0
- neue cc - async/awaitのキャンセル処理やタイムアウトを効率的に扱うためのパターン&プラクティス
- Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.