C#中基于任务的异步方法的超时模式

时间:2014-09-05 10:38:16

标签: c# .net design-patterns asynchronous async-await

据我所知,有两种可能的模式可以实现基于任务的异步方法的超时:

内置超时

public Task DoStuffAsync(TimeSpan timeout)

这种方法难以实现,因为对整个调用堆栈实现全局超时并不容易。例如,Web API控制器接收HTTP请求并调用DoStuffAsync,并且调用者希望全局超时为3秒。

也就是说,每个内部异步方法调用都需要接收已经使用过的时间的减法...

没有内置超时

public Task DoStuffAsync(CancellationToken cancellationToken)

..........

CancellationTokenSource cancellationSource = new CancellationTokenSource();
Task timeoutTask = Task.Delay(3000);

if(await Task.WhenAny(DoStuffAsync(cancellationTokenSource), timeoutTask) == timeoutTask)
{
     cancellationSource.Cancel();

     throw new TimeoutException();
}

这似乎是最可靠和易于实现的模式。第一个调用者定义全局超时,如果超时,则所有挂起的操作都将被取消。此外,它为直接调用者提供取消令牌,内部调用将共享相同的取消令牌引用。因此,如果最大调用者超时,它将能够取消任何工作线程。

整个问题

我是否缺少任何模式,或者如果我使用无内置超时开发API,我是否正确?

2 个答案:

答案 0 :(得分:13)

虽然可以重复使用WithCancellation取消和超时,但我认为这对你所需要的东西来说太过分了。

async操作超时的更简单,更清晰的解决方案是await实际操作和使用Task.WhenAny的超时任务。如果超时任务首先完成,则会超时。否则,操作成功完成:

public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    if (task == await Task.WhenAny(task, Task.Delay(timeout)))
    {
        return await task;
    }
    throw new TimeoutException();
}

用法:

try
{
    await DoStuffAsync().WithTimeout(TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
    // Handle timeout.
}

如果您不想抛出异常(就像我一样),它甚至更简单,只需返回默认值:

public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, timeoutTask).Unwrap();
}

答案 1 :(得分:11)

  

我是否缺少任何模式,或者如果我使用no内置超时开发API,我是否正确?

<强>声明:

当我们在取消状态中谈论Task时,我们的意思是我们在进行时取消操作。当我们谈论取消时,这可能不是这种情况,因为如果在指定的间隔之后完成任务,我们就会丢弃任务。在下面的Stephan Toubs文章中讨论了为什么BCL不提供取消正在进行的操作的OOTB功能的原因。


我现在看到的常见方法是 no build-in 方法,我发现自己主要用于实现取消机制。绝对是两者中更容易的,留下最高帧负责取消,同时传递内部帧取消令牌。如果您发现自己重复此模式,则可以使用已知的WithCancellation扩展方法:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken)
{
    var cancellationCompletionSource = new TaskCompletionSource<bool>();

    using (cancellationToken.Register(() => cancellationCompletionSource.TrySetResult(true)))
    {
        if (task != await Task.WhenAny(task, cancellationCompletionSource.Task))
        {
            throw new OperationCanceledException(cancellationToken);
        }
    }

    return await task;
}

这是来自Stephan Toubs How do I cancel non-cancelable async operations?,它并不完全符合您的要求,但绝对值得一读。

Task Cancellation docs继续指定两种取消任务的方式:

  

您可以使用以下选项之一终止操作:

     
      
  1. 只需从代表返回即可。在许多情况下,这已足够;但是,以这种方式取消的任务实例将转换为TaskStatus.RanToCompletion状态,而不是TaskStatus.Canceled状态。

  2.   
  3. 通过抛出OperationCanceledException并将请求取消的令牌传递给它。执行此操作的首选方法是使用ThrowIfCancellationRequested方法。以这种方式取消的任务转换为Canceled状态,调用代码可以使用该状态验证任务是否响应其取消请求

  4.   

修改

至于您关注使用TimeSpan指定所需的时间间隔,请使用带有TimeSpan参数的overload of CancellationTokenSource constructor

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

var task = Task.Run(() => DoStuff()).WithCancellation(cts.Token);