vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Taskをキャンセルする

はじめに

前回に引き続き、async / await / Task ネタです。

非同期で処理している内容をストップするにはどうするの?というお話。

Task.Runの中で処理を止める

Task.Run の中で、条件に応じて処理を途中で止める、というのは比較的簡単です。

private async void CallAsyncMethod()
{
    var myTask = await GenerateTextAsync();
    // GenerateTextAsync() の処理が完了したら実行される.
    Debug.Log(myTask);
}
private async Task< string> GenerateTextAsync()
{  
  return await Task.Run< string>( () => {

    if(何かの条件)
    {
      return "Canceled";
    }
  

    Thread.Sleep( 3000 );
    const string sampleText = "Hello\nworld\nおはようございます!\n";

    return sampleText;
  } );
}

普通の処理と同じく、早期リターンするか後の処理を if でくくって Task を完了させてしまえば良いですね。

Task.Runの外から処理を止める

では Task.Run の外側から処理を止めるにはどうするか。

感覚的には Task.Cancel とかすれば良い気がしたのですが、そのようなメソッドはありません。

方法としては、
1. Task.Run の第二引数として、 CancellationToken という Token を渡します。 2. Task.Run の外側(処理を止めたいタイミングで) CancellationToken.Cancel() を実行します。 3. Task.Run のなかで CancellationToken.IsCancellationRequested が True かどうかを確認し、 True なら処理を止めます。

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

public class MainController : MonoBehaviour {
    public Text ResultText;
    private int flag;
    private void Start ()
    {
    
    // 非同期処理をCancelするためのTokenを取得.
        var tokenSource = new CancellationTokenSource();
        var cancelToken = tokenSource.Token;
        

        CallAsyncMethod(cancelToken);
        Debug.Log("Start()");

  
    // 処理をキャンセル.
        tokenSource.Cancel();
    

    }
    private async void CallAsyncMethod(CancellationToken cancelToken)
    {       
        var myTask = await GenerateTextAsync(cancelToken);
        Debug.Log(myTask);
    }
    private async Task GenerateTextAsync(CancellationToken cancelToken)
    {
        var context = SynchronizationContext.Current;

        return await Task.Run( () => {
            Thread.Sleep( 1000 );

        
            if (cancelToken.IsCancellationRequested)
            {
                Debug.Log("Canceled");
                // キャンセルされたらTaskを終了する.
                return "Canceled";
            }
      

            const string sampleText = "Hello\nworld\nおはようございます!\n";

            Thread.Sleep( 2000 );

            context.Post((state) =>
            {
                ResultText.text = sampleText;
            }, cancelToken);
            return sampleText;
        }, cancelToken);
    }
}

回りくどい気もしますが、 どのような処理を行っているかを確認せず、いきなり強制終了するな、ということなのではないでしょうか。

Token を、 GenerateTextAsync を呼んでいる CallAsyncMethod() ではなく、さらに上流となる Start() で生成・渡しているのは、
前回書いた通り await を使って非同期処理を呼ぶと、それ以後の処理は非同期処理が完了するまで実行されないためです。

なお、処理がキャンセルされたときに例外を投げたい場合は、 cancelToken.ThrowIfCancellationRequested() を使ってシンプルに書くことができます。

if (cancelToken.IsCancellationRequested)
{
  Debug.Log("Canceled");
  // キャンセルされたら OperationCanceledException を投げる.
  cancelToken.ThrowIfCancellationRequested();
}

注意すべき点としては、キャンセルされたときに処理を止める、という処理は自分で書かないといけない、という点です。
あくまでもキャンセルされたことが Task.Run の中で判別できるようにする、という仕組みのようなので。

おわりに

Task がキャンセルされうる、というのは引数に Token を渡すことで判断はできるのですが、
本当にキャンセル処理が含まれているか、というのは静的に確認できると良さそうな気はしました
(とはいえキャンセルフラグが立ったときに、必ずしもその場で処理をストップするとは限らないので難しいかもですが)。

まだちゃんと調べられてはいませんが、 Task は別の Task とまとめたりもできるようなので、
見通しが悪くならない程度に細分化して、必要な処理が揃っているかを確認する、というのが良いのかもしれませんね。

参照