The main difference between your tasks is the overload for the Task.Run method you're using:
task1 is created with Task.Run Method (Func<Task>), rather than task2 is created with Task.Run<TResult> Method (Func<TResult>). This overloads do create task with a little bit difference:
- for
task1 the Result property is set to System.Threading.Tasks.VoidTaskResult, and CreationOptions is set to None,
- rather than
task2 the CreationOptions is set to DenyChildAttach, and the result is a default(int), which is 0.
When you are waiting the task2, the Result property isn't being set to real value, because the exception is thrown. According MSDN:
When a task instance observes an OperationCanceledException thrown by
user code, it compares the exception's token to its associated token
(the one that was passed to the API that created the Task). If they
are the same and the token's IsCancellationRequested property returns
true, the task interprets this as acknowledging cancellation and
transitions to the Canceled state. If you do not use a Wait or WaitAll
method to wait for the task, then the task just sets its status to
Canceled.
If you are waiting on a Task that transitions to the
Canceled state, a System.Threading.Tasks.TaskCanceledException
exception (wrapped in an AggregateException exception) is thrown. Note
that this exception indicates successful cancellation instead of a
faulty situation. Therefore, the task's Exception property returns
null.
If the token's IsCancellationRequested property returns false or
if the exception's token does not match the Task's token, the
OperationCanceledException is treated like a normal exception, causing
the Task to transition to the Faulted state. Also note that the
presence of other exceptions will also cause the Task to transition to
the Faulted state. You can get the status of the completed task in the
Status property.
So, here we can find the reason for this behavior - the exception is treated like a normal exception because of the token mismatch. This is strange, because the token is definitely the same (I've checked that in Debug, the hash code is equal, Equals method and double equals operator returns true), but the comparison still returns false. So, the solution for your case is explicit usage of the cancellation tokens, something like this (I've added the Thread.Sleep to avoid the race condition):
var t1TokenSource = new CancellationTokenSource();
var t1 = Task.Run(() =>
{
Thread.Sleep(1000);
if (t1TokenSource.Token.IsCancellationRequested)
{
t1TokenSource.Token.ThrowIfCancellationRequested();
}
//throw new TaskCanceledException();
}, t1TokenSource.Token);
try
{
t1TokenSource.Cancel();
t1.Wait();
}
catch (AggregateException e)
{
Debug.Assert(t1.IsCanceled);
}
var t2TokenSource = new CancellationTokenSource();
var t2 = Task.Run(() =>
{
Thread.Sleep(1000);
if (t2TokenSource.Token.IsCancellationRequested)
{
t2TokenSource.Token.ThrowIfCancellationRequested();
}
//throw new TaskCanceledException();
return 1;
}, t2TokenSource.Token);
try
{
t2TokenSource.Cancel();
t2.Wait();
}
catch (AggregateException e)
{
Debug.Assert(t2.IsCanceled);
}
Another quote from MSDN:
You can terminate the operation by using one of these options:
- By simply returning from the delegate. In many scenarios this is sufficient; however, a task instance that is canceled in this way
transitions to the
TaskStatus.RanToCompletion state, not to the
TaskStatus.Canceled state.
- By throwing a
OperationCanceledException and passing it the token on which cancellation was requested. The preferred way to do this is
to use the ThrowIfCancellationRequested method. A task that is
canceled in this way transitions to the Canceled state, which the
calling code can use to verify that the task responded to its
cancellation request.
As you can see, the preffered way is working predictably, the direct exception throw does not. Please also note that in case of usage task is created with DenyChildAttach too, and doesn't have a Result property, so there is some difference in constructors, which you've faced with.
Hope this helps.