Reputation: 366
Why does this test fail?
The only difference between t1
and t2
, as far as I can tell, is that t1
is a Task
, and t2
is a Task<int>
. Yet for some reason t2
ends up in the Faulted
status, as opposed to the Canceled
status. Why would the behavior differ?
[Test]
public void Test_foo()
{
var t1 = Task.Run(() =>
{
throw new OperationCanceledException();
});
try
{
t1.Wait();
}
catch (AggregateException e)
{
Assert.IsTrue(t1.IsCanceled);
}
var t2 = Task.Run(() =>
{
throw new OperationCanceledException();
return 1;
});
try
{
t2.Wait();
}
catch (AggregateException e)
{
Assert.IsTrue(t2.IsCanceled); // fails, it's Faulted
}
}
Upvotes: 2
Views: 108
Reputation: 28355
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:
task1
the Result
property is set to System.Threading.Tasks.VoidTaskResult
, and CreationOptions
is set to None
,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 theTask
). If they are the same and the token'sIsCancellationRequested
property returns true, the task interprets this as acknowledging cancellation and transitions to theCanceled
state. If you do not use aWait
orWaitAll
method to wait for the task, then the task just sets its status toCanceled
.If you are waiting on a Task that transitions to the
Canceled
state, aSystem.Threading.Tasks.TaskCanceledException
exception (wrapped in anAggregateException
exception) is thrown. Note that this exception indicates successful cancellation instead of a faulty situation. Therefore, the task'sException
property returns null.If the token's
IsCancellationRequested
property returnsfalse
or if the exception's token does not match the Task's token, theOperationCanceledException
is treated like a normal exception, causing the Task to transition to theFaulted
state. Also note that the presence of other exceptions will also cause theTask
to transition to theFaulted
state. You can get the status of the completed task in theStatus
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 theTaskStatus.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 theThrowIfCancellationRequested
method. A task that is canceled in this way transitions to theCanceled
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.
Upvotes: 2