2016年11月24日
川俣晶の縁側ソフトウェア技術雑記total 6954 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

表形式のまとめ §

titleexceptionFlagprocFlag
Cancel before Run with no argumentTrueFalse
Cancel after Run with no argumentTrueFalse
Cancel after 200ms from Run with no argumentFalseTrue
Cancel before Run FalseTrueFalse
Cancel after Run FalseTrueFalse
Cancel after 200ms from Run FalseFalseTrue
Cancel before Run TrueTrueFalse
Cancel after Run TrueTrueFalse
Cancel after 200ms from Run TrueFalseTrue

Facebook

キーワード【 川俣晶の縁側ソフトウェア技術雑記
【技術雑記】の次のコンテンツ
2016年
11月
30日
1年を経過したUWPのプロジェクトの扱い方
3days 0 count
total 3305 count
【技術雑記】の前のコンテンツ
2016年
11月
23日
USBポートにAndroidスマホを接続しておくと再起動が止まる問題
3days 0 count
total 3182 count

このコンテンツを書いた川俣 晶へメッセージを送る

[メッセージ送信フォームを利用する]

メッセージ送信フォームを利用することで、川俣 晶に対してメッセージを送ることができます。

この機能は、100%確実に川俣 晶へメッセージを伝達するものではなく、また、確実に川俣 晶よりの返事を得られるものではないことにご注意ください。

このコンテンツへトラックバックするためのURL

https://mag.autumn.org/tb.aspx/20161124183521
サイトの表紙【技術雑記】の表紙【技術雑記】のコンテンツ全リスト 【技術雑記】の入手全リスト 【技術雑記】のRSS1.0形式の情報このサイトの全キーワードリスト 印刷用ページ

管理者: 川俣 晶連絡先

Powered by MagSite2 Version 0.36 (Alpha-Test) Copyright (c) 2004-2021 Pie Dey.Co.,Ltd.