問題 §
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 |