【问题标题】:Timeout pattern on task-based asynchronous method in C#C#中基于任务的异步方法的超时模式
【发布时间】:2014-10-30 07:05:06
【问题描述】:

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

内置超时

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,我的方法是否正确?

【问题讨论】:

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


【解决方案1】:

是否有任何我遗漏的模式,或者如果我使用无内置超时开发 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;
}

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

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

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

  1. 通过简单地从代理返回。在许多情况下,这已经足够了;但是,以这种方式取消的任务实例会转换为 TaskStatus.RanToCompletion 状态,而不是 TaskStatus.Canceled 状态。

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

编辑

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

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

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

【讨论】:

  • 这是可行的,尽管“取消”意味着操作没有(也不会)完成,这对于这段代码来说不一定是正确的。引用Toub,这包含在 BCL 中,“因为担心它会在不考虑后果的情况下过快使用而成为拐杖,这是微妙的。”
  • 我认为 OP 理解这里所说的“取消”的语义。虽然我会编辑我的答案以明确这一点。
  • @YuvalItzchakov 完美!谢谢
  • 有没有办法为多个任务实现这一点(使用Task.WhenAll)?我已经尝试过了,但是从一项任务中抛出异常会导致其他任务也被中止。 PS - 我需要使用await 而不是Task.Run,因为我需要在任务中使用HttpContext
  • @YuvalItzchakov 您在上面提供并在 2012 年 (blogs.msdn.microsoft.com/pfxteam/2012/10/05/…) 撰写的 Stephen 的 Taub 文章真的在 2019 年仍然适用吗?难道我们现在没有更好的方法吗?很抱歉问,因为我在使用 NetworkStream 的 ReadAsync 和 WriteAsync 时读到了很多抱怨,而且很多抱怨都是从那个时候开始的,而且微软在这方面的文档很烂
【解决方案2】:

虽然您可以在取消和超时中重复使用 WithCancellation,但我认为这对您的需要来说有点过头了。

对于async 操作超时,一个更简单、更清晰的解决方案是使用Task.WhenAnyawait 进行实际操作和超时任务。如果超时任务首先完成,您将获得超时。否则,操作成功完成:

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

【讨论】:

  • 嘿。实际上,这就是我在没有像您的扩展方法那样概括的情况下提出问题之前所做的事情,但是您是对的,大多数情况下这应该起作用。顺便说一句,我不确定控制异常流是否是个好主意。我不会那样做的。我会返回一个带有布尔值的元组来指定操作是否完成
  • @MatíasFidemraizer 使用异常是 .Net 库的运行方式。取消时也会抛出。我确实更喜欢避免这些异常,但我不是返回元组,而是返回默认值(添加到答案中)。另一种选择是使用TryX(out result) 范式。
  • @i3arnon 解释如何实现 NetworkStream 的 ReadAsync 和 WriteAsync 取消的任何地方。我无处可去,根据这篇文章stackoverflow.com/questions/54816114/…,我一直陷入僵局
  • @cd491415 你是什么意思实施?你有自己的NetworkStream?一个是 .NET 不会覆盖 WriteAsync/ReadAsync,因此您会得到 Stream 实现,如果它在操作开始之前被取消,它只会查看 CancellationToken。因此,这些操作并不是真正可以取消的。
  • @i3arnon 不,我说的是 .NET 的 NetworkStream。我试图弄清楚如何正确使用 WriteAsync 和 ReadAsync,因为我无法在 Xamarin.Forms 的主线程上使用网络。所以,我必须使用 WriteAsync 来获取远程信息,然后如果有响应,使用 ReadAsync 读取我得到的响应。
猜你喜欢
  • 2017-04-22
  • 1970-01-01
  • 1970-01-01
  • 2014-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多