Taskのキャンセルのしかた その2

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

以前の自分の記事で、Taskのキャンセルの仕方を調べた。

そこで、なんとなくキャンセルの仕方がわかったのだが、下記の記事を拝見して、さらに便利にキャンセルする方法があると知った。

neue.cc

そこで知ったことを自分なりにまとめたい。

やったこと

複数の理由でキャンセルして、OperationCanceledExceptionに入っているtokenを見てどの理由かを知る

  • CancellationTokenSource.CreateLinkedTokenSource()で複数のTokenをつなげる。
  • つなげたTokenで、キャンセルを検知し、OperationCanceledExceptionをthrowする。
  • throwされたOperationCanceledExceptionをcatchしたときに、つなげたtokenのどれのIsCancellationRequestedが立っているかを見る。

という手順で見れる。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace CancellationTokenJikken
{
    public partial class MainWindow : Window
    {
        CancellationTokenSource cts;
        CancellationTokenSource cts2;

        public MainWindow() => InitializeComponent();

        private async Task MugenLoop(CancellationToken token1, CancellationToken token2)
        {
            using var linked = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);

            try
            {
                await Task.Delay(10000, linked.Token);
            }
            catch (OperationCanceledException tce) when (tce.CancellationToken == linked.Token)
            {
                if (tce.CancellationToken.IsCancellationRequested)
                {
                    if (token1.IsCancellationRequested)
                        Debug.WriteLine("トークン1でキャンセル");
                    if (token2.IsCancellationRequested)
                        Debug.WriteLine("トークン2でキャンセル");
                }
            }
        }
        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            cts = new CancellationTokenSource();
            cts2 = new CancellationTokenSource();
            await MugenLoop(cts.Token, cts2.Token);
        }

        // キャンセル1
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            cts.Cancel();
        }

        // キャンセル2
        private void Button_Click_3(object sender, RoutedEventArgs e)
        {
            cts2.Cancel();
        }
    }
}

「.NETのクラスライブラリ設計 」のP303のページ下部に書かれてる「訳注」が、これのことかな?と思う。

タイムアウトをCancellationTokenSourceで実装する

CancellationTokenSourceにはCancelAfter(TimeSpan)というメソッドがあり、それでタイムアウトを設定できる。

タイムアウトというのはつまり、tokenを指定の秒数で自動でCancelするようにできるということ。
使い方は下記のようにする。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace CancellationTokenJikken
{
    public partial class MainWindow : Window
    {
        CancellationTokenSource cts;

        public MainWindow() => InitializeComponent();

        private async Task MugenLoop(CancellationToken cancellationToken)
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromSeconds(5));// 5秒でキャンセル

            await Task.Run(() =>
            {
                try
                {
                    while (true)
                    {
                        // この無限に続くループをキャンセルorタイムアウトさせる
                        Thread.Sleep(1000);
                        cts.Token.ThrowIfCancellationRequested();
                    }
                }
                catch (OperationCanceledException tce) when (tce.CancellationToken == cts.Token)
                {
                    if (cancellationToken.IsCancellationRequested)
                    {
                        Debug.WriteLine("外のtokenでキャンセル");
                    }
                    else
                    {
                        Debug.WriteLine("タイムアウトでキャンセル");
                    }
                }
            }).ConfigureAwait(false);
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            cts = new CancellationTokenSource();

            await MugenLoop(cts.Token);
        }

        // キャンセル1
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            cts.Cancel();
        }
    }
}

キャンセルされたことをCallbackで知る

下記のようにすると、CancellationTokenSourceがキャンセルされたときに呼ばれるActionを設定できる。 正直、どういうときに役立つかは呑み込めてない。

※参考にさせて頂いたこちらでは、こちらのtokenがキャンセルされたら、別のtokenもキャンセルする、みたいな使い方をされている様子だが、詳細は難しくて見切れなかった...

cts = new CancellationTokenSource();

cts.Token.Register(() =>
{
    Debug.WriteLine("キャンセルされました");
});

参考

async/awaitのキャンセル処理やタイムアウトを効率的に扱うためのパターン&プラクティス

https://neue.cc/2022/07/13_Cancellation.html

.NETのクラスライブラリ設計
この本の「9.2 非同期パターン」がめちゃ参考になった。