【问题标题】:C# Task Ignoring Cancellation timeoutC# 任务忽略取消超时
【发布时间】:2015-05-22 03:28:10
【问题描述】:

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

我有以下测试和实现

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

应该发生的情况是测试方法尝试花费 >3000 毫秒并且超时应该发生在 20 毫秒,但这并没有发生。为什么我的代码没有按预期超时?

编辑:

根据要求-decoratedPolicy如下

public async Task ExecuteAsync(Action action, CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    await Task.Factory.StartNew(action.Invoke, token);  
}

【问题讨论】:

  • 你在监控_decoratedPolicy.ExecuteAsync(action, cts.Token);里面的token吗?
  • 通过监控你到底是什么意思?抱歉 - 这是第一次涉足该领域。这是当前内部“装饰策略” public async Task ExecuteAsync(Action action, CancellationToken token) { await Task.Factory.StartNew(action.Invoke, token); }
  • 要解决您的 TODO,只需创建一个 linked cancelation token source var cts = CancellationTokenSource.CreateLinkedTokenSource(token);。这会创建一个“子源”,可以取消但不会取消传入的令牌,如果传入的令牌被取消,它也会自行取消。编辑:最后一点。链接令牌源是一个好主意,最好将令牌源包装在 using 语句中,否则您可能会得到一些无法被 GC 处理的长寿命对象
  • 不错!我什至没有想过那个 TODO,但谢谢你:)

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


【解决方案1】:

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

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

【讨论】:

  • 但他的方法确实支持取消。
  • @YuvalItzchakov 即使这样做了,也不能保证它会及时响应。我读了他的问题,因为它不支持取消,即使它需要CancellationToken
  • 但是你只是忽略了执行,没有什么真正被取消。如果他负责实现 API,他应该让令牌通过需要的地方,而不是丢弃任务。
  • @YuvalItzchakov 你是对的。我之前没有取消原来的任务。现在修改了我的答案以取消任务仍然遵循相同的技术。
  • 链接令牌是一个将 CancellationTokenSource 放在 using 语句中的好主意,因为它在处置期间是 unregisters the link。 (如果您使用接受TimeSpan 的构造函数,或者您执行使用内部WaitHandle 的操作,您也应该使用它。
【解决方案2】:

使用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 将被丢弃,而不是实际被取消。

【讨论】:

  • hmm 仍然不起作用 - 我已经在上面包含了我的装饰方法。我的“做事”方法是否也需要接受和监控令牌?
  • @PaulDevenney 所有相关参与者都需要监控令牌。如果DoStuff 是您传递的Action,那么是的。
【解决方案3】:

您正在调用 Assert.Throws(Action action) 并且您的匿名异步方法被强制转换为 async void。该方法将使用 Fire&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);
}

【讨论】:

    【解决方案4】:

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

    根本原因/解决方案:

    await Task.Factory.StartNew(action.Invoke, token);
    

    你在上面我的“装饰策略”中看到的返回一个 Task 并且 await 只等待外部任务。替换为

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

    得到正确的结果。

    我的代码遇到了 Gotcha #4Gotcha #5 的组合问题,来自 C# async gotchas

    上的一篇优秀文章

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

    【讨论】:

    • 我认为你在这里遗漏了一些东西,关于Task.Factory.StartNew 的问题是当你传递一个 Func 委托时,在你的情况下你传递的是一个动作,所以等待将在动作完成后返回执行。您不能像在回答中那样等待 Action。我认为根本原因实际上是 Assert.Throws 不处理异步方法。
    • 除了上面的更改修复了功能,并且从 Assert.Throws 的更改产生了零差异。无论我是否正确解释了确切原因,原件都不允许调用返回
    猜你喜欢
    • 1970-01-01
    • 2020-06-10
    • 2015-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多