2016年11月24日
川俣晶の縁側ソフトウェア技術雑記 total 6952 count

.NET FrameworkのCancellationTokenを利用してタスクをキャンセルすると振る舞いが2種類ある問題

Written By: 川俣 晶連絡先

問題 §

 CancellationTokenを使ったタスクのキャンセル処理の結果が一定しない。

結論 §

 CancellationTokenを指定してAPIを呼び出すとき、タイミングによる2種類の振る舞いを起こすことが分かった。

 API呼び出し前またはAPI呼び出し直後にCancelメソッドを実行すると、例外が発生し、指定された処理は行われない。

 API呼び出し後十分な時間を置いてからCancelメソッドを実行すると、例外は発生せず指定された処理が実行される。その際、IsCancellationRequestedはCancelメソッドを呼び出した時点でTrueになる。

 Cancelメソッドを無引数で使用するか、引数にfalseを指定するか、trueを指定するかで振る舞いに差は生じない。

暫定的な推奨事項 §

 つまり、非同期にCancelメソッドを呼び出す処理を記述する場合、その結果となる振る舞いが2種類あることを念頭に置いてコードを書く必要がある。

 注意することは、キャンセル可能な処理の実行を開始しているから例外は起きない……という前提を置けないことである。つまり、非同期処理をキャンセル可能な処理の実行を開始した後でのみCancelメソッドを呼び出すという方法では、2種類の振る舞いを一種類にまとめる効能が期待できない。

 (おそらく、指定された処理が始まってしまえば、例外は起きないと思われるのだが、ユーザーコードを実行するRunメソッド等では処理が始まったことを検出できるが、そうではない場合は検出できないかもしれない)

検証ソースコード §

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace CancellationToken001

{

    enum cancelPosition {

        beforeRun,

        immediateRun,

        after200ms,

    };

    class Program

    {

        private static string getLabelForNullableBool(bool? b)

        {

            if (b == null) return "with no argument";

            return b.ToString();

        }

        private static void cancel(System.Threading.CancellationTokenSource source, bool? cancelArg)

        {

            //Console.WriteLine("canceling {0}", getLabelForNullableBool(cancelArg));

            if (cancelArg == null) source.Cancel();

            else source.Cancel((bool)cancelArg);

        }

        private static List<Tuple<string, bool, bool>> results = new List<Tuple<string, bool, bool>>();

        private static void sub3(string title, cancelPosition cancelMode, bool? cancelArg)

        {

            bool exceptionFlag = false;

            bool procFlag = false;

            Console.WriteLine("***** {0}, {1} *****", title, getLabelForNullableBool(cancelArg));

            System.Threading.CancellationTokenSource cancellationTokenSource;

            System.Threading.CancellationToken cancellationToken;

            Console.WriteLine("Start {0}", title);

            cancellationTokenSource = new System.Threading.CancellationTokenSource();

            cancellationToken = cancellationTokenSource.Token;

            if (cancelMode == cancelPosition.beforeRun) cancel(cancellationTokenSource, cancelArg);

            var task = Task.Run(() =>

            {

                procFlag = true;

                Console.WriteLine("Start Task {0}, IsCancellationRequested={1}", title, cancellationToken.IsCancellationRequested);

                Task.Delay(1000).Wait();

                Console.WriteLine("End Task {0}, IsCancellationRequested={1}", title, cancellationToken.IsCancellationRequested);

            }, cancellationToken);

            if (cancelMode == cancelPosition.immediateRun) cancel(cancellationTokenSource, cancelArg);

            if (cancelMode == cancelPosition.after200ms) Task.Delay(200).ContinueWith((t)=> cancel(cancellationTokenSource, cancelArg));

            Console.WriteLine("Waiting {0}", title);

            try

            {

                task.Wait();

            }

            catch (AggregateException e)

            {

                exceptionFlag = true;

                Console.WriteLine(e);

            }

            Console.WriteLine("Done {0}", title);

            results.Add(new Tuple<string, bool, bool>(title + " " + getLabelForNullableBool(cancelArg), exceptionFlag, procFlag));

        }

        private static void sub4(bool? cancelArg)

        {

            sub3("Cancel before Run", cancelPosition.beforeRun, cancelArg);

            sub3("Cancel after Run", cancelPosition.immediateRun, cancelArg);

            sub3("Cancel after 200ms from Run", cancelPosition.after200ms, cancelArg);

        }

        static void Main(string[] args)

        {

            Task.Run(() =>

            {

                sub4(null);

                sub4(false);

                sub4(true);

            });

            Task.Delay(9000*3).Wait();

            Console.WriteLine("All Done");

            Console.WriteLine("title,exceptionFlag,procFlag");

            foreach (var item in results)

            {

                Console.WriteLine("{0},{1},{2}", item.Item1,item.Item2,item.Item3);

            }

        }

    }

}

実行結果(全て) §

***** Cancel before Run, with no argument *****

Start Cancel before Run

Waiting Cancel before Run

System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Threading.Tasks.TaskCanceledException: タスクが取り消されました。

--- 内部例外スタック トレースの終わり ---

場所 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

場所 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

場所 System.Threading.Tasks.Task.Wait()

場所 CancellationToken001.Program.sub3(String title, cancelPosition cancelMode, Nullable`1 cancelArg) 場所 C:\xgit\Study\CancellationToken001\CancellationToken001\Program.cs:行 55

---> (内部例外 #0) System.Threading.Tasks.TaskCanceledException: タスクが取り消 されました。<---

Done Cancel before Run

***** Cancel after Run, with no argument *****

Start Cancel after Run

Waiting Cancel after Run

System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Threading.Tasks.TaskCanceledException: タスクが取り消されました。

--- 内部例外スタック トレースの終わり ---

場所 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

場所 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

場所 System.Threading.Tasks.Task.Wait()

場所 CancellationToken001.Program.sub3(String title, cancelPosition cancelMode, Nullable`1 cancelArg) 場所 C:\xgit\Study\CancellationToken001\CancellationToken001\Program.cs:行 55

---> (内部例外 #0) System.Threading.Tasks.TaskCanceledException: タスクが取り消 されました。<---

Done Cancel after Run

***** Cancel after 200ms from Run, with no argument *****

Start Cancel after 200ms from Run

Waiting Cancel after 200ms from Run

Start Task Cancel after 200ms from Run, IsCancellationRequested=False

End Task Cancel after 200ms from Run, IsCancellationRequested=True

Done Cancel after 200ms from Run

***** Cancel before Run, False *****

Start Cancel before Run

Waiting Cancel before Run

System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Threading.Tasks.TaskCanceledException: タスクが取り消されました。

--- 内部例外スタック トレースの終わり ---

場所 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

場所 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

場所 System.Threading.Tasks.Task.Wait()

場所 CancellationToken001.Program.sub3(String title, cancelPosition cancelMode, Nullable`1 cancelArg) 場所 C:\xgit\Study\CancellationToken001\CancellationToken001\Program.cs:行 55

---> (内部例外 #0) System.Threading.Tasks.TaskCanceledException: タスクが取り消 されました。<---

Done Cancel before Run

***** Cancel after Run, False *****

Start Cancel after Run

Waiting Cancel after Run

System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Threading.Tasks.TaskCanceledException: タスクが取り消されました。

--- 内部例外スタック トレースの終わり ---

場所 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

場所 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

場所 System.Threading.Tasks.Task.Wait()

場所 CancellationToken001.Program.sub3(String title, cancelPosition cancelMode, Nullable`1 cancelArg) 場所 C:\xgit\Study\CancellationToken001\CancellationToken001\Program.cs:行 55

---> (内部例外 #0) System.Threading.Tasks.TaskCanceledException: タスクが取り消 されました。<---

Done Cancel after Run

***** Cancel after 200ms from Run, False *****

Start Cancel after 200ms from Run

Start Task Cancel after 200ms from Run, IsCancellationRequested=False

Waiting Cancel after 200ms from Run

End Task Cancel after 200ms from Run, IsCancellationRequested=True

Done Cancel after 200ms from Run

***** Cancel before Run, True *****

Start Cancel before Run

Waiting Cancel before Run

System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Threading.Tasks.TaskCanceledException: タスクが取り消されました。

--- 内部例外スタック トレースの終わり ---

場所 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

場所 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

場所 System.Threading.Tasks.Task.Wait()

場所 CancellationToken001.Program.sub3(String title, cancelPosition cancelMode, Nullable`1 cancelArg) 場所 C:\xgit\Study\CancellationToken001\CancellationToken001\Program.cs:行 55

---> (内部例外 #0) System.Threading.Tasks.TaskCanceledException: タスクが取り消 されました。<---

Done Cancel before Run

***** Cancel after Run, True *****

Start Cancel after Run

Waiting Cancel after Run

System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Threading.Tasks.TaskCanceledException: タスクが取り消されました。

--- 内部例外スタック トレースの終わり ---

場所 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

場所 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

場所 System.Threading.Tasks.Task.Wait()

場所 CancellationToken001.Program.sub3(String title, cancelPosition cancelMode, Nullable`1 cancelArg) 場所 C:\xgit\Study\CancellationToken001\CancellationToken001\Program.cs:行 55

---> (内部例外 #0) System.Threading.Tasks.TaskCanceledException: タスクが取り消 されました。<---

Done Cancel after Run

***** Cancel after 200ms from Run, True *****

Start Cancel after 200ms from Run

Start Task Cancel after 200ms from Run, IsCancellationRequested=False

Waiting Cancel after 200ms from Run

End Task Cancel after 200ms from Run, IsCancellationRequested=True

Done Cancel after 200ms from Run

All Done

title,exceptionFlag,procFlag

Cancel before Run with no argument,True,False

Cancel after Run with no argument,True,False

Cancel after 200ms from Run with no argument,False,True

Cancel before Run False,True,False

Cancel after Run False,True,False

Cancel after 200ms from Run False,False,True

Cancel before Run True,True,False

Cancel after Run True,True,False

Cancel after 200ms from Run True,False,True

表形式のまとめ §

title exceptionFlag procFlag
Cancel before Run with no argument True False
Cancel after Run with no argument True False
Cancel after 200ms from Run with no argument False True
Cancel before Run False True False
Cancel after Run False True False
Cancel after 200ms from Run False False True
Cancel before Run True True False
Cancel after Run True True False
Cancel after 200ms from Run True False True