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

Mat*_*zer 8 .net c# design-patterns asynchronous async-await

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

内置超时

public Task DoStuffAsync(TimeSpan timeout)
Run Code Online (Sandbox Code Playgroud)

这种方法难以实现,因为实现整个调用堆栈的全局超时并不容易.例如,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();
}
Run Code Online (Sandbox Code Playgroud)

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

整个问题

有没有我缺少的模式,或者如果我使用no内置超时开发API,我是否以正确的方式?

i3a*_*non 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();
}
Run Code Online (Sandbox Code Playgroud)

用法:

try
{
    await DoStuffAsync().WithTimeout(TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
    // Handle timeout.
}
Run Code Online (Sandbox Code Playgroud)

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

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();
}
Run Code Online (Sandbox Code Playgroud)


Yuv*_*kov 11

有没有我缺少的模式,或者如果我使用no内置超时开发API,我是否以正确的方式?

免责声明:

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


我现在看到的常见方法是没有内置方法,我发现自己主要用于实现取消机制.绝对是两者中更容易的,留下最高帧负责取消,同时传递内部帧取消令牌.如果您发现自己重复此模式,则可以使用已知的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;
}
Run Code Online (Sandbox Code Playgroud)

这是来自Stephan Toubs 如何取消不可取消的异步操作?这并不完全符合您的要求,但绝对值得一读.

任务取消文档去指定任务取消的方法有两种:

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

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

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

编辑

至于你使用a TimeSpan来指定所需的间隔,请使用带有参数CancellationTokenSource构造函数重载TimeSpan:

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

var task = Task.Run(() => DoStuff()).WithCancellation(cts.Token);
Run Code Online (Sandbox Code Playgroud)