C#任务忽略取消超时

时间:2015-05-20 13:44:17

标签: c# async-await task-parallel-library task

我试图为任意代码编写一个包装器,它会在给定的超时时间后取消(或至少停止等待)代码。

我有以下测试和实施

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

和&#34; DoStuff&#34;方法

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

4 个答案:

答案 0 :(得分:4)

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

通常,这是通过启动具有所需超时值的计时器来完成的。如果计时器首先触发,那么您可以抛出异常。使用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方法,而是忽略它。

如果您的方法支持取消(但不及时),则最好在超时后取消任务。您可以通过创建链接令牌来完成。

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

答案 1 :(得分:2)

使用CancellationToken表示您正在进行合作取消。设置CancellationTokenSource.CancelAfter会在指定的时间后将基础标记转换为已取消状态,但如果该标记未被调用异步方法监视,则不会发生任何事情。

为了实际生成OperationCanceledException,您需要在cts.Token.ThrowIfCancellationRequested内拨打_decoratedPolicy.ExecuteAsync

例如:

// 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被丢弃的地方,而不是实际取消。

答案 2 :(得分:1)

您正在调用Assert.Throws(Action操作),并且您的匿名异步方法将转换为async void。该方法将与Fire&amp; Forget语义异步调用,而不会抛出异常。

然而,由于异步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);
}

答案 3 :(得分:0)

我决定在这里回答我自己的问题,因为虽然列出的每个答案都解决了我需要做的事情,但他们没有确定这个问题的根本原因。很多,非常感谢:Scott Chamberlain,Yuval Itzchakov,Sriram Sakthivel,Jeff Cyr。感谢所有的建议。

根本原因/解决方案:

await Task.Factory.StartNew(action.Invoke, token);
你在我的“装饰策略”中看到的

返回一个Task,并且await只等待外部任务。用

替换它
await Task.Run(async () => await action.Invoke());

获得正确的结果。

我的代码受到C# async gotchas

上一篇优秀文章的 Gotcha#4 Gotcha#5 的影响。

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