【UniTask】UniTaskのキャンセル

UniTask
使用しているツールのバージョン
  • Unity: 2022.3.35f1
  • UniTask: 2.5.4

UniTaskのキャンセルの基本

UniTaskは、完了までに時間がかかる非同期処理です。非同期処理を実行する際、ある条件でその処理を完了前に取りやめたいことが良くあります。例えば、サーバーからデータを取得しようとしたが、取得に時間がかかりすぎている場合に取得を中止し、通信に失敗した旨をユーザーに伝えたい場合です。

今回は、UniTaskの実行中にその処理を中止する方法について学んでいきます。

CancellationTokenオブジェクト

UniTaskは、CancellationToken オブジェクトを使用してキャンセルすることができます。例として、次のコードを作成し、適当なオブジェクトにアタッチして実行しましょう。

SampleManager.cs
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class SampleManager : MonoBehaviour
{
	private CancellationTokenSource cancellationTokenSource = new();

	private void Start()
	{
		CancellationToken cancellationToken = cancellationTokenSource.Token;
		_ = StartTimerAsync("Timer A", 3, cancellationToken);
	}

	private void Update()
	{
		if (Input.GetKeyDown(KeyCode.Space))
		{
			Debug.Log("Spacekey pushed!");
			cancellationTokenSource.Cancel();
		}
	}

	private void OnDestroy()
	{
		cancellationTokenSource?.Cancel();
		cancellationTokenSource?.Dispose();
	}

	private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
	{
		Debug.Log($"{name} started...");
		while (seconds > 0)
		{
			Debug.Log($"{name}: {seconds} sec.");
			seconds--;
			await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
		}
		Debug.Log($"{name} finished!");
	}
}
C#

実行してから何もせずにいると、指定した秒数でタイマーが終了します。

実行してから、指定秒数が経過するまでにスペースキーを押すとタイマーが途中で止まります。

UniTaskがキャンセルされる仕組みについてみていきます。

まず、StartTimerAsync メソッドはパラメータとしてCancellationToken オブジェクトを要求します。CancellationToken オブジェクトはUniTaskのキャンセルを引き起こすオブジェクトであり、様々な方法で作成することができます。ここでは、CancellationTokenSource のインスタンスを作成し、そこからCancellationToken オブジェクトを取得しています。

CancellationTokenをCancellationTokenSourceから取得
private CancellationTokenSource cancellationTokenSource = new();

private void Start()
{
	CancellationToken cancellationToken = cancellationTokenSource.Token;
	_ = StartTimerAsync("Timer A", 3, cancellationToken);
}
C#

StartTimerAsync メソッドがパラメータとして受け取ったCancellationToken オブジェクトは、UniTask.Delay メソッドに渡されます。UniTask.Delay メソッドは、渡されたCancellationToken オブジェクトがキャンセルされたときにOperationCanceledException 例外を送出し、処理を中断させす。

UniTask.DelayにCancellationTokenを渡す
await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
C#

CancellationTokenSource より取得したCancellationToken オブジェクトは、取得元のCancellationTokenSource インスタンスのCancel メソッドが呼び出されたときに、キャンセルされます。ここでは、スペースキーを押したときに、CancellationTokenSource のインスタンスがキャンセルされ、それによりCancellationToken オブジェクトがキャンセルされるようになっています。

スペースキー押下時にCancellationTokenSourceをキャンセルする
private void Update()
{
	if (Input.GetKeyDown(KeyCode.Space))
	{
		Debug.Log("Spacekey pushed!");
		cancellationTokenSource.Cancel();
	}
}
C#

まとめると、次の手順でStartTimerAsync メソッドがキャンセルされます。

  • スペースキーが押される
  • CancellationTokenSourceCancel メソッドが呼ばれる
  • StartTimerAsync メソッドに渡されたCancellationToken オブジェクトがキャンセルされる
  • UniTask.Delay メソッドから、OperationCanceledException 例外が送出される
  • 例外により、StartTimerAsync メソッドの処理が中断される

なお、CancellationTokenSourceIDisposable インターフェースを実装しているので、プログラム終了時に必ずDispose メソッドを呼び出すようにします。また、Dispose を呼び出す前、Cancel を呼び出してキャンセルもしておきます。これは、そのCancellationTokenSource 由来のCancellationToken が渡されたUniTaskの処理がプログラムの終わりで終了することを保証するためです。これについては、後に詳しく学びます。

プログラム終了時にCancellationTokenSourceがキャンセルされ、Disposeされることを保証する
private void OnDestroy()
{
	cancellationTokenSource?.Cancel();
	cancellationTokenSource?.Dispose();
}
C#

キャンセル時の処理を定義する

UniTaskのキャンセルは、OperationCanceledException 例外が送出されることで引き起こされます。そのため、try-catch 構文により、UniTaskがキャンセルされたときの動作を記述することができます。

UniTaskキャンセル時の動作を定義する
private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
{
	try
	{
		Debug.Log($"{name} started...");
		while (seconds > 0)
		{
			Debug.Log($"{name}: {seconds} sec.");
			seconds--;
			await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
		}
		Debug.Log($"{name} finished!");
	}
	catch (OperationCanceledException)
	{
		Debug.Log($"{name} canceled!");
		throw;
	}
}
C#

try-catch-finally を使用すれば、キャンセルされたかされていないかに関わらず、必ず実行したい処理を記述することもできます。

キャンセルに関係なく実行される処理を定義する
private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
{
	try
	{
		Debug.Log($"{name} started...");
		while (seconds > 0)
		{
			Debug.Log($"{name}: {seconds} sec.");
			seconds--;
			await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
		}
		Debug.Log($"{name} finished!");
	}
	catch (OperationCanceledException)
	{
		Debug.Log($"{name} canceled!");
		throw;
	}
	finally
	{
		Debug.Log("From finally...");
	}
}
C#
キャンセルなしで完了する場合
キャンセルされる場合
OperationCanceledException例外を再度スローするか

UniTaskがキャンセルされた際に送出されるOperationCanceledException 例外は、例外の一つなので、catch で例外をキャッチした後に、throw; によって再度例外をするかどうかによって、処理の挙動が変わります。

例えば、次の二つのコードはキャンセル時に異なる結果を示します。

例外を再度スローする
private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
{
	try
	{
		Debug.Log($"{name} started...");
		while (seconds > 0)
		{
			Debug.Log($"{name}: {seconds} sec.");
			seconds--;
			await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
		}
		Debug.Log($"{name} finished!");
	}
	catch (OperationCanceledException)
	{
		Debug.Log($"{name} canceled!");
		throw;
	}
	Debug.Log("End of method");
}
C#
例外を再度スローしない
private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
{
	try
	{
		Debug.Log($"{name} started...");
		while (seconds > 0)
		{
			Debug.Log($"{name}: {seconds} sec.");
			seconds--;
			await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
		}
		Debug.Log($"{name} finished!");
	}
	catch (OperationCanceledException)
	{
		Debug.Log($"{name} canceled!");
	}
	Debug.Log("End of method");
}
C#

UniTaskのキャンセル時、1つ目のコードではメッセージ”End of method”が表示されませんが、2つ目のコードでは表示されます。

これは、1つ目のコードでは例外がcatch 文の中で再度送出されたことによりtry-catch 文以降のメソッドの処理が中断されるのに対し、2つ目のコードではcatch 文の中で例外を握りつぶしているために、try-catch 文以降の処理が例外の影響を受けないためです。

OperationCanceledExeption 例外をcatch 文でキャッチすることでUniTaskがキャンセルされた場合の処理を記述する場合は、そのキャンセルをなかったことにしたいのか、他の場所へそのキャンセルを伝播させたいのかを場面に合わせて考える必要があります。

キャンセルの判定を自身で定義する

前の例でのUniTaskのキャンセルは、CancellationToken がキャンセルされ、それが渡されていたUniTask.Delay からOperationCanceledException 例外が送出することで引き起こされていました。

基本的には、そのあたりの処理はUniTask.Delay のようなすでに用意されているメソッドに任せればよいですが、自分自身でCancellationToken オブジェクトがキャンセルされているかを確認し、例外を送出することもできます。

キャンセル判定をし、OperationCanceledExceptionを投げる
private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
{
	Debug.Log($"{name} started...");
	while (seconds > 0)
	{
		Debug.Log($"{name}: {seconds} sec.");
		seconds--;
		await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
		if (cancellationToken.IsCancellationRequested)
		{
			throw new OperationCanceledException();
		}
	}
	Debug.Log($"{name} finished!");
}
C#

上の例では、CancellationToken がキャンセルされているかをcancellationToken.IsCancellationRequested により確認し、キャンセルされている場合に例外を送出しています。

これは、次のような書き方もできます。

IsCancellationRequestedを使用してキャンセルを判定する
private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
{
	Debug.Log($"{name} started...");
	while (seconds > 0)
	{
		Debug.Log($"{name}: {seconds} sec.");
		seconds--;
		await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
		cancellationToken.ThrowIfCancellationRequested();
	}
	Debug.Log($"{name} finished!");
}
C#

ThrowIfCancellationRequested は、CancellationToken のキャンセルの状態を判定し、キャンセルされていればOperationCanceledException 例外を送出します。

なお、このようなコードはUniTask.Delay に直接CancellationToken を渡した場合とは少し挙動が異なることに注意してください。UniTask.Delay に直接CancellationToken を渡した場合はキャンセル時に処理が即座に中断されますが、上の例では、キャンセル時には必ずUniTask.Delay の処理が完了してから例外が送出され、後続の処理が中断されることになります(UniTask.Dealy の指定秒数を長めに設定すると、違いが分かりやすいです)。

CancellationTokenの作り方

前のセクションの例では、UniTaskのキャンセルに必要なCancellationToken オブジェクトを、CancellationTokenSource のインスタンスから取得していました。

実際には、CancellationTokenSource だけでなく、いくつかのCancellationToken を取得する方法があります。ここからは、それらについて学習します。

CancellationTokenSourceから取得する

CancellationToken を取得する一番基本的な方法が、すでに例で確認した通り、CancellationTokenSource のインスタンスから取得する方法です。

実は、CancellationTokenCancellationTokenSource もC#標準で用意されているものであり、C#標準のTask を使用した非同期処理のキャンセルに使われます。System.Threading 名前空間で定義されています。

CancellationTokenSourceからCancellationTokenを取得する
using System.Threading;

// CancellationTokenSourceのインスタンスの作成
CancellationTokenSource cancellationTokenSource = new();
// CancellationTokenオブジェクトの取得
CancellationToken cancellationToken = cancellationTokenSource.Token;
// 好きなタイミングでCancelationTokenをキャンセル
cancellationTokenSource.Cancel();
// 使い終わったら必ずDisposeする
cancellationTokenSource.Dispose();
C#

UniTaskから変換する

CancellationToken を、UniTaskから変換して取得することができます。このCancellationToken がキャンセルされるのは、変換元のUniTaskが終了したときです。

UniTaskを変換してCancellationTokenを取得する
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class SampleManager : MonoBehaviour
{
	private void Start()
	{
		CancellationToken tokenFromUniTask = StartTimerAsync("Timer A", 2, default).ToCancellationToken();
		_ = StartTimerAsync("Timer B", 5, tokenFromUniTask);
	}

	private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
	{
		try
		{
			Debug.Log($"{name} started...");
			while (seconds > 0)
			{
				Debug.Log($"{name}: {seconds} sec.");
				seconds--;
				await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
				await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
			}
			Debug.Log($"{name} finished!");
		}
		catch (OperationCanceledException)
		{
			Debug.Log($"{name} canceled!");
		}
	}
}
C#

この例では、StartTimerAsync メソッドを、秒数をそれぞれ2秒(Timer A)と5秒(Timer B)で指定して、2回呼び出しています。Timer AのUniTaskはToUniTask メソッドによってCancellationToken オブジェクトに変換され、Timer BのUniTaskに渡されています。

プログラムを実行すると、指定された秒数はTimer Aの方が短いので、Timer AのUniTaskの方が先に終了します。そして、Timer AのUniTaskが終了すると、そこから変換されたCancellationToken がキャンセルされ、それが渡されているTimer BのUniTaskがキャンセルされます。

このように、ToCancellationToken メソッドでUniTaskを変換することで、変換元のUniTaskが終了したときにキャンセルされるようなCancellationToken を作ることができます。

GameObjectから生成する

CancellationTokenGetCancellationTokenOnDestroy メソッドによってGameObjectから取得することができ、この時、CancellationToken は取得元のGameObjectが破棄されるとき、キャンセルされます。

GameObjectからCancellationTokenを取得する
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class SampleManager : MonoBehaviour
{
	private async UniTaskVoid Start()
	{
		GameObject sourceObject = new();
		CancellationToken tokenFromObject = sourceObject.GetCancellationTokenOnDestroy();
		_ = StartTimerAsync("Timer A", 2, tokenFromObject);
		await UniTask.Delay(TimeSpan.FromSeconds(1));
		Destroy(sourceObject);
	}

	private async UniTask StartTimerAsync(string name, int seconds, CancellationToken cancellationToken)
	{
		try
		{
			Debug.Log($"{name} started...");
			while (seconds > 0)
			{
				Debug.Log($"{name}: {seconds} sec.");
				seconds--;
				await UniTask.Delay(TimeSpan.FromSeconds(1.0f), cancellationToken: cancellationToken);
			}
			Debug.Log($"{name} finished!");
		}
		catch (OperationCanceledException)
		{
			Debug.Log($"{name} canceled!");
		}
	}
}
C#

この例では、まず1つのGameObject を作成し、そこからGetCancellationTokenOnDestroy メソッドによりCancellationToken オブジェクトを得ています。そして、得られたオブジェクトを、StartTimerAsync メソッドに渡しています。メソッド呼び出し後は、1秒待機した後にCancellationToken の取得元であるGameObject を破棄するようにしています。

Destroy によってGameObjecy が破棄されると、そのオブジェクトに対するGetCancellationTokenOnDestroy メソッド呼び出しによって得られたCancellationToken はキャンセルされます。そのため、上の例ではオブジェクトの破棄と同時にUniTaskがキャンセルされています。

この方法は、例えば終了することがないUniTaskに渡すCancellationToken の作成に向いています。後に学習しますが、すべてのUniTaskは、それが不必要になったときに必ずキャンセルされることを保証しなければなりません。そうしなければ、リソースリークを引き起こすかもしれないからです。

UniTaskが不必要になるタイミングでよくあるのは、現在のシーンから新たなシーンへと移行するときです。シーンの遷移時、そのシーンでのみ必要なUniTaskは、新たなシーンではすべて不必要になるためキャンセルされる必要があります。シーン遷移時、古いシーンにあるオブジェクトはすべて破棄されるので、GetCancellationTokenOnDestroy によって取得したCancellationToken をUniTaskに渡して実行すると、そのUniTaskはシーン遷移時、自動的にキャンセルされることになります。

シーン遷移時に自動的にキャンセルされるCancellationTokenを取得する
CancellationToken tokenFromObject = this.GetCancellationTokenOnDestroy();
// 終了しないUniTask
_ = NeverFinishAsync(tokenFromObject);
// ここでシーン遷移が起こり、オブジェクトが破棄されトークンもキャンセルされる
SceneManager.LoadScene("Next Scene");
C#

CancellationTokenSourceOnDestroy を組み合わせて同様のことができますが、コードは冗長になります。

CancellationTokenSource版
private CancellationTokenSource cancellationTokenSource = new();

private void Start()
{
	CancellationToken cancellationToken = cancellationTokenSource.Token;
	_ = NeverFinishAsync(cancellationToken);
}

private void OnDestroy()
{
	cancellationTokenSource?.Cancel();
	cancellationTokenSource?.Dispose();
}
C#

UniTaskの終了を保証する

UniTaskにCancellationToken を渡すことで、UniTaskの実行をキャンセルできることを学習しました。ここで、CancellationToken はUniTaskにキャンセルのために渡すオプションの引数と考えるよりは、特別な理由がない限りは必ず渡す引数であると考えた方が良いです。なぜなら、すべてのUniTaskは、それが不必要になったときに、キャンセルされたか正常に完了したかを問わず処理が終了していることを保証しなければならないからです。

不必要になったUniTaskを必ず終了させないといけない理由を理解するため、1つの例を見てみます。

悪い例
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SampleManager : MonoBehaviour
{
	private static int count = 0;
	
	private async UniTaskVoid Start()
	{
		count++;
		_ = RepeatMessageAsync($"Message from scene {count}");
		await UniTask.Delay(TimeSpan.FromSeconds(1));
		SceneManager.LoadScene(SceneManager.GetActiveScene().name);
	}

	private async UniTask RepeatMessageAsync(string message)
	{
		while (true)
		{
			Debug.Log(message);
			await UniTask.Delay(TimeSpan.FromSeconds(1));
		}
	}
}
C#

RepeatMessageAsync メソッドは、指定されたメッセージを1秒ごとに、永遠に表示し続けるUniTaskです。Start メソッドでは、このメソッドを呼び出し、1秒後に現在のシーンを再度読み込むという処理を行っています。なお、何回目に読み込まれたシーンからメソッドが呼び出されているのかを判別するため、静的フィールド変数のcount を定義しています。

これを実行すると、以下のような結果になります。

結果からわかる通り、シーンの遷移が発生したとしても、UniTaskの処理は終わりません。シーンを新たに読み込むごとに、実行中であるUniTaskの数は際限なく増えていきます。ここでは明らかなリソースリークが発生しており、そのうちメモリ不足によりアプリケーションはクラッシュするかもしれません。

重要なのは、UniTaskと、それを呼び出したGameObjectの寿命の間には何にも関係がないということです。新たなシーンが読み込まれ、UniTaskを呼び出したGameObjectが破棄されたとしても、UniTaskは残り続けます。そのため、UniTaskが不必要になった時点で、リソースリークを避けるため、処理が完了していないUniTaskはキャンセルし、その処理が終了することを保証する必要があるのです。

上のコードを正しく書き直すと、次のようになります。

良いコード
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SampleManager : MonoBehaviour
{
	private static int count = 0;
	private async UniTaskVoid Start()
	{
		count++;
		_ = RepeatMessageAsync($"Message from scene {count}", this.GetCancellationTokenOnDestroy());
		await UniTask.Delay(TimeSpan.FromSeconds(1));
		SceneManager.LoadScene(SceneManager.GetActiveScene().name);
	}

	private async UniTask RepeatMessageAsync(string message, CancellationToken cancellationToken)
	{
		while (true)
		{
			Debug.Log(message);
			await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: cancellationToken);
		}
	}
}
C#

このコードでは、RepeatMessageAsyncCancellationToken によってキャンセルできるようになっており、また、その呼び出し時にはGetCancellationTokenOnDestroy によって生成したCancellationToken を渡しています。そのため、新しいシーンの読み込み時にはSampleManager オブジェクトが破棄され、それに結び付いたCancellationToken がキャンセルされ、古いシーンで呼び出されたUniTaskが停止することを保証することができます。結果を見ても、シーンが切り替わった時点で、古いシーンからのメッセージの表示は停止していることがわかります。

繰り返しますが、すべてのUniTaskについて、それが不必要になったときにその処理が停止していることを保証しなければなりません。永遠に終了しないUniTaskが残り続けると、そのうち実行中のUniTaskの数が膨大になってアプリケーションのクラッシュを引き起こしたり、あるいは意図せずに残っているUniTaskにより、意図しない動作が引き起こされたりするかもしれないからです。呼び出すUniTaskが、そのシーン内ではキャンセルされず、永遠に動作し続けるものだとしても、必ずCancellationToken を渡し、そのシーンの終了時にはキャンセルされるようにしなければいけません。

UniTask Tracker

すべてのUniTaskが、処理を完了もしくは適切にキャンセルされることを保証しようとしても、意図せずに残り続けるUniTaskが生まれてしまうことは良くあります。そこで、UniTask Trackerによって、意図せずに動作が続いているUniTaskがないかどうかをチェックすることができます。

上部メニューのWindow/UniTaskTrackerからUniTask Trackerを開き、Enable AutoReloadとEnable Trackingを有効にしたうえで、悪いコードと良いコードのそれぞれを実行してみます。

UniTaskの終了を保証しないコードの実行結果
UniTaskの終了を保証するコードの実行結果

悪いコードの実行時にはPending状態のUniTaskが続々と増えていき、良いコードの実行時には、新たなPending状態のUniTaskが出現すると同時に、古いUniTaskがCanceled状態になって停止していることがわかります。

UniTaskを活用して開発を進める際には、定期的にUniTask Trackerにより、適切に停止していないUnitaskがないかどうかをチェックすると良いでしょう。

演習

CanellationTokenSourceを使用したUniTaskのキャンセル

以下のプログラムを変更して、スペースキーを押したときにTask AとTask Bが、エンターキーを押したときにTask Cがそれぞれキャンセルされるようにしましょう。CancellationToken の生成には、CancellationTokenSource を使用します。

ちなみに、StartTaskAsync メソッドはキャンセルされるまでは終了しないUniTaskです。UniTask.DelayFrame は、指定したフレーム分だけ遅延させるUniTaskで、ライブラリのUniTaskから提供されています。

C#
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class SampleManager : MonoBehaviour
{
	private void Start()
	{
		_ = StartTaskAsync("Task A", default);
		_ = StartTaskAsync("Task B", default);
		_ = StartTaskAsync("Task C", default);
	}

	private void Update()
	{
		if (Input.GetKeyDown(KeyCode.Space))
		{
			Debug.Log("Spacekey pushed!");
		}
		if (Input.GetKeyDown(KeyCode.Return))
		{
			Debug.Log("Returnkey pushed!");
		}
	}

	private async UniTask StartTaskAsync(string name, CancellationToken cancellationToken)
	{
		try
		{
			Debug.Log($"{name} started...");
			while (true)
			{
				await UniTask.DelayFrame(1, cancellationToken: cancellationToken);
			}
		}
		catch (OperationCanceledException)
		{
			Debug.Log($"{name} canceled!");
			throw;
		}
	}
}
C#
実行例

UniTaskをCancellationTokenに変換する

前問のプログラムを変更し、CancellationTokenSource を使わず、UniTaskから変換して得られたCancellationToken を使用してUniTaskをキャンセルできるようにしましょう。つまり、前問のプログラムにおけるUpdate メソッドの中で行われていたキーの押下判定の処理をUniTaskで置き換え、そこからCancellationToken を得ます。

解答例

CanellationTokenSourceを使用したUniTaskのキャンセル
C#
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class SampleManager : MonoBehaviour
{
	CancellationTokenSource spacekeyTokenSource = new();
	CancellationTokenSource returnkeyTokenSource = new();

	private void Start()
	{
		_ = StartTaskAsync("Task A", spacekeyTokenSource.Token);
		_ = StartTaskAsync("Task B", spacekeyTokenSource.Token);
		_ = StartTaskAsync("Task C", returnkeyTokenSource.Token);
	}

	private void Update()
	{
		if (Input.GetKeyDown(KeyCode.Space))
		{
			Debug.Log("Spacekey pushed!");
			spacekeyTokenSource.Cancel();
		}
		if (Input.GetKeyDown(KeyCode.Return))
		{
			Debug.Log("Returnkey pushed!");
			returnkeyTokenSource.Cancel();
		}
	}

	private void OnDestroy()
	{
		spacekeyTokenSource?.Cancel();
		spacekeyTokenSource?.Dispose();
		returnkeyTokenSource?.Cancel();
		returnkeyTokenSource?.Dispose();
	}

	private async UniTask StartTaskAsync(string name, CancellationToken cancellationToken)
	{
		try
		{
			Debug.Log($"{name} started...");
			while (true)
			{
				await UniTask.DelayFrame(1, cancellationToken: cancellationToken);
			}
		}
		catch (OperationCanceledException)
		{
			Debug.Log($"{name} canceled!");
			throw;
		}
	}
}
C#

CancellationTokenSourceのキャンセル(不要になった時点でUniTaskの停止を保証するため)と、Dispose(CancellationTokenSourceIDisposable インターフェースを実装するため)を忘れないようにしましょう。

UniTaskをCancellationTokenに変換する
C#
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class SampleManager : MonoBehaviour
{
	private void Start()
	{
		var spacekeyToken = WaitUntilSpacekeyPushed(this.GetCancellationTokenOnDestroy()).ToCancellationToken();
		var returnkeyToken = WaitUntilReturnkeyPushed(this.GetCancellationTokenOnDestroy()).ToCancellationToken();
		_ = StartTaskAsync("Task A", spacekeyToken);
		_ = StartTaskAsync("Task B", spacekeyToken);
		_ = StartTaskAsync("Task C", returnkeyToken);
	}

	private async UniTask WaitUntilSpacekeyPushed(CancellationToken cancellationToken)
	{
		while (!Input.GetKeyDown(KeyCode.Space))
		{
			await UniTask.DelayFrame(1, cancellationToken: cancellationToken);
		}
	}
	
	private async UniTask WaitUntilReturnkeyPushed(CancellationToken cancellationToken)
	{
		while (!Input.GetKeyDown(KeyCode.Return))
		{
			await UniTask.DelayFrame(1, cancellationToken: cancellationToken);
		}
	}

	private async UniTask StartTaskAsync(string name, CancellationToken cancellationToken)
	{
		try
		{
			Debug.Log($"{name} started...");
			while (true)
			{
				await UniTask.DelayFrame(1, cancellationToken: cancellationToken);
			}
		}
		catch (OperationCanceledException)
		{
			Debug.Log($"{name} canceled!");
			throw;
		}
	}
}
C#

CancellationToken に変換するUniTaskを呼び出す際、やはりGetCancellationTokenOnDestroy によって、不要になった時点で二つのUniTaskの処理が停止することを保証します。

コメント