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);  
}

async await - c#任务忽略取消超时

如果我理解正确的话,您是在尝试为不支持超时/取消的方法支持超时。

这通常是通过启动具有所需超时值的计时器来完成的。如果计时器先触发,则可以抛出异常。对于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 #4Gotcha #5的影响,这是一篇关于c#异步陷阱的优秀文章

整篇文章(以及对这个问题的回答)确实提高了我的整体理解。