async await - c#任务忽略取消超时
本文关键字:取消 超时 任务 await async | 更新日期: 2023-09-27 17:49:54
我正在尝试为任意代码编写一个包装器,该包装器将在给定的超时时间后取消(或至少停止等待)代码。
我有以下测试和实现
[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
Assert.Throws<TimeoutException>(async () => await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token));
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
和"DoStuff"方法
private static async Task DoStuff(int sleepTime, IFakeService fakeService)
{
await Task.Delay(sleepTime).ConfigureAwait(false);
var result = await Task.FromResult("bob");
var test = result + "test";
fakeService.DoStuff();
}
和IExecutionPolicy的实现。ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
var cts = new CancellationTokenSource();//TODO: resolve ignoring the token we were given!
var task = _decoratedPolicy.ExecuteAsync(action, cts.Token);
cts.CancelAfter(_timeout);
try
{
await task.ConfigureAwait(false);
}
catch(OperationCanceledException err)
{
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms", err);
}
}
应该发生的是,测试方法试图花费>3000ms,超时应该发生在20ms,但这并没有发生。为什么我的代码没有超时?
编辑:按要求- decoratedPolicy如下
public async Task ExecuteAsync(Action action, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.Factory.StartNew(action.Invoke, token);
}
如果我理解正确的话,您是在尝试为不支持超时/取消的方法支持超时。
这通常是通过启动具有所需超时值的计时器来完成的。如果计时器先触发,则可以抛出异常。对于TPL,您可以使用Task.Delay(_timeout)
代替定时器。
public async Task ExecuteAsync(Action action, CancellationToken token)
{
var task = _decoratedPolicy.ExecuteAsync(action, token);
var completed = await Task.WhenAny(task, Task.Delay(_timeout));
if (completed != task)
{
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
}
}
注意:这不会停止_decoratedPolicy.ExecuteAsync
方法的执行,而是忽略它。
如果你的方法支持取消(但不是及时的),那么最好在超时后取消Task。您可以通过创建一个链接令牌来实现。
public async Task ExecuteAsync(Action action, CancellationToken token)
{
using(var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token))
{
var task = _decoratedPolicy.ExecuteAsync(action, linkedTokenSource.Token);
var completed = await Task.WhenAny(task, Task.Delay(_timeout));
if (completed != task)
{
linkedTokenSource.Cancel();//Try to cancel the method
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
}
}
}
使用CancellationToken
意味着您正在进行合作取消。设置CancellationTokenSource.CancelAfter
将在指定的时间后将底层令牌转换为取消状态,但如果该令牌未被调用异步方法监视,则不会发生任何事情。
为了实际生成OperationCanceledException
,您需要在_decoratedPolicy.ExecuteAsync
中调用cts.Token.ThrowIfCancellationRequested
。
// Assuming this is _decoratedPolicy.ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
// This is what creates and throws the OperationCanceledException
token.ThrowIfCancellationRequested();
// Simulate some work
await Task.Delay(20);
}
编辑:为了实际取消令牌,您需要在执行工作和执行可能超时的所有点监视它。如果您不能保证,那么请遵循@SriramSakthivel的答案,其中实际的Task
被丢弃,而不是实际取消。
您正在调用Assert。抛出(Action Action),你的匿名异步方法被强制转换为async void。该方法将以Fire&Forget语义异步调用,而不会抛出异常。
然而,由于async void方法中未捕获的异常,进程可能很快崩溃。
你应该同步调用ExecuteAsync:
[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
Assert.Throws<AggregateException>(() => policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token).Wait());
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
或者使用异步测试方法:
[Test]
public async Task Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
try
{
await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token);
Assert.Fail("Method did not timeout.");
}
catch (TimeoutException)
{ }
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
我决定在这里回答我自己的问题,因为虽然列出的每个答案都解决了我需要做的事情,但它们并没有确定这个问题的根本原因。非常非常感谢:斯科特·张伯伦,尤瓦尔·伊察科夫,斯里拉姆·萨克蒂维尔,杰夫·西尔。所有的建议,我都很感激。
/根本原因的解决方案:
await Task.Factory.StartNew(action.Invoke, token);
,你可以在上面的"装饰策略"中看到,它返回一个Task, await只等待外部任务。用
替换await Task.Run(async () => await action.Invoke());
得到正确的结果。
我的代码受到了Gotcha #4和Gotcha #5的影响,这是一篇关于c#异步陷阱的优秀文章
整篇文章(以及对这个问题的回答)确实提高了我的整体理解。