问题描述
为什么这个测试失败了?
据我所知,t1
和 t2
之间的唯一区别是 t1
是一个 Task
,而 t2
是一个 Task
.然而出于某种原因,t2
最终处于 Faulted
状态,而不是 Canceled
状态.为什么行为会不同?
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
}
}
推荐答案
您的任务之间的主要区别在于您使用的 Task.Run
方法的重载:
The main difference between your tasks is the overload for the Task.Run
method you're using:
task1
是使用 Task 创建的.Run 方法 (Func)
,而不是 task2
是用 Task.Run方法(Func)
.这个重载确实创建了一个有点不同的任务:
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
,结果
属性设置为System.Threading.Tasks.VoidTaskResult
和CreationOptions
设置为None
, - 而不是
task2
CreationOptions
设置为DenyChildAttach
,结果是一个default(int)
,也就是0
.
- for
task1
theResult
property is set toSystem.Threading.Tasks.VoidTaskResult
, andCreationOptions
is set toNone
, - rather than
task2
theCreationOptions
is set toDenyChildAttach
, and the result is adefault(int)
, which is0
.
当您等待 task2
时,Result
属性未设置为实际值,因为抛出了异常.根据 MSDN:
当任务实例观察到 OperationCanceledException
抛出用户代码,它将异常的令牌与其关联的令牌进行比较(传递给创建 Task
的 API 的那个).如果他们是相同的和令牌的 IsCancellationRequested代码>
属性返回true,任务将此解释为确认取消并转换到 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.
如果令牌的 IsCancellationRequested
属性返回 false
或如果异常的令牌与任务的令牌不匹配,则OperationCanceledException
是视为正常异常,导致转换到 Faulted
状态的任务.还要注意的是其他异常的存在也会导致 Task
转换为Faulted
状态.您可以在状态
财产.
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.
所以,在这里我们可以找到这种行为的原因 - 由于令牌不匹配,异常被视为正常异常.这很奇怪,因为令牌是绝对相同(我已经在调试中检查过,哈希码是相等的,Equals
方法和双等号运算符返回 true
),但比较仍然返回 false
.因此,针对您的情况的解决方案是显式使用 取消令牌,类似这样(我添加了 Thread.Sleep
以避免竞争条件):
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);
}
来自 MSDN 的另一句话:
您可以使用以下选项之一终止操作:
- 通过简单地从委托返回.在许多情况下,这已经足够了;但是,以这种方式取消的任务实例转换到
TaskStatus.RanToCompletion
状态,而不是TaskStatus.Canceled
状态. - 通过抛出
OperationCanceledException
并将请求取消的令牌传递给它.执行此操作的首选方法是使用ThrowIfCancellationRequested
方法. 一个任务是以这种方式取消转换到Canceled
状态,调用代码可以用来验证任务是否响应了它的取消请求.
- 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.
如您所见,首选方式可预测地工作,而直接异常抛出则不然.另请注意,如果使用 task
也是使用 DenyChildAttach
创建的,并且没有 Result
属性,因此您遇到的构造函数存在一些差异.
希望这会有所帮助.
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.
这篇关于任务行为不同于 Task<T>抛出 OperationCanceledException 时的行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!