【Unity】UniTaskのキャンセル処理まとめ・Taskとの比較

UniTaskのキャンセル処理についてまとめました。

Unity2020.1.10
UniTask 2.0.37

非同期メソッドのキャンセル方法

まず非同期メソッドのキャンセル方法についてまとめます。

Taskの場合

UniTaskについて書く前にTaskについてまとめます。
Taskをキャンセルにするには、以下のようにCancellationTokenを非同期メソッドに渡します。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class Example : MonoBehaviour
{
    [SerializeField] private Button _cancelButton;
    
    private async void Start()
    {
        // CancellationTokenSourceを生成
        var cts = new CancellationTokenSource();

        _cancelButton.onClick.AddListener(() =>
        {
            cts.Cancel();
        });
        
        // CancellationTokenSource.Tokenを非同期メソッドに渡す
        await ExampleAsync(cts.Token);
    }
    
    private async Task ExampleAsync(CancellationToken cancellationToken = default)
    {
        // この時点でキャンセルされていたらOperationCanceledExceptionを投げるメソッド
        cancellationToken.ThrowIfCancellationRequested();

        for (var i = 0; i < 5; i++) {
            // TaskのファクトリメソッドにcancellationTokenを渡せる
            await Task.Run(() => Thread.Sleep(1000), cancellationToken);
        }
    }
}

キャンセルすると例外がスローされるのでそれをハンドリングします。

UniTaskの場合

UniTaskの場合も基本的にはTaskと同じ方法でキャンセル処理を行います。

using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class Example : MonoBehaviour
{
    [SerializeField] private Button _cancelButton;
    
    private async void Start()
    {
        // CancellationTokenSourceを生成
        var cts = new CancellationTokenSource();

        _cancelButton.onClick.AddListener(() =>
        {
            cts.Cancel();
        });
        
        // CancellationTokenSource.Tokenを非同期メソッドに渡す
        await ExampleAsync(cts.Token));
    }
    
    private async UniTask ExampleAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        for (var i = 0; i < 5; i++) {
            await Task.Run(() => Thread.Sleep(1000), cancellationToken);
        }
    }
}
UnityのAsyncOperation系

UniTaskではUnityのAsyncOperation系の構造体にGetAwaiter()を実装していますが、
これらについては引数にCancellationTokenを渡すことができません。
これらには以下のようにWithCancellation()という拡張メソッドが定義されているので、これを使ってCancellationTokenを渡します。

using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class Example : MonoBehaviour
{
    [SerializeField] private Button _cancelButton;
    
    private async void Start()
    {
        // CancellationTokenSourceを生成
        var cts = new CancellationTokenSource();

        _cancelButton.onClick.AddListener(() =>
        {
            cts.Cancel();
        });

        // UnityのAsyncOperationにAwaiterを定義してある系のものにはWithCancellationで渡す
        await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token);
    }
}
GameObjectのライフサイクルに紐づいたCancellationToken

UniTaskではGameObjectのライフサイクルと紐づいたCancellationTokenを取得することができます。
これを非同期メソッドに渡すと、GameObjectが破棄されたらキャンセルされる処理が実装できます。

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

public class Example : MonoBehaviour
{    
    private async void Start()
    {        
        // GameObjectがDestroyされたときにキャンセルされるCancellationTokenを生成する
        await ExampleAsync(this.GetCancellationTokenOnDestroy());
    }
    
    private async UniTask ExampleAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        for (var i = 0; i < 5; i++) {
            await Task.Run(() => Thread.Sleep(1000), cancellationToken);
        }
    }
}

キャンセルのハンドリング方法

次に非同期メソッドキャンセルのハンドリング方法についてまとめます。

基本的にはtry-catch

非同期メソッドがキャンセルされたときにはOperationCanceledExceptionという例外がスローされます。
TaskでもUniTaskでも基本的にはこの例外をtry-catchしてハンドリングします。

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

public class Example : MonoBehaviour
{
    private async void Start()
    {
        try
        {
            await ExampleAsync(this.GetCancellationTokenOnDestroy());
        }
        catch (OperationCanceledException e)
        {
            // OperationCanceledExceptionをキャッチしてキャンセルをハンドリング
            Debug.Log("キャンセルされた");
            throw;
        }
    }
    
    private async UniTask ExampleAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        for (var i = 0; i < 5; i++) {
            await Task.Run(() => Thread.Sleep(1000), cancellationToken);
        }
    }
}
SuppressCancellationThrowでキャンセル状態を戻り値として得る

UniTaskの場合には以下のようにUniTask.SuppressCancellationThrow()を使うことでキャンセル状態を戻り値として受け取ることができます。

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

public class Example : MonoBehaviour
{
    private async void Start()
    {
        // SuppressCancellationThrowによりキャンセル状態が戻り値として返される
        var canceled = await ExampleAsync(this.GetCancellationTokenOnDestroy()).SuppressCancellationThrow();
        if (canceled)
        {
            Debug.Log("キャンセルされた");
        }
    }
    
    private async UniTask ExampleAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        for (var i = 0; i < 5; i++) {
            await Task.Run(() => Thread.Sleep(1000), cancellationToken);
        }
    }
}