【问题标题】:Asynchronously wait for Task<T> to complete with timeout异步等待 Task<T> 超时完成
【发布时间】:2010-11-21 14:29:16
【问题描述】:

我想等待 Task<T> 完成一些特殊规则: 如果 X 毫秒后仍未完成,我想向用户显示一条消息。 如果 Y 毫秒后还没有完成,我想自动request cancellation

我可以使用Task.ContinueWith 异步等待任务完成(即安排在任务完成时执行的操作),但这不允许指定超时。 我可以使用Task.Wait 同步等待任务超时完成,但这会阻塞我的线程。 如何异步等待任务超时完成?

【问题讨论】:

  • 你是对的。我很惊讶它没有提供超时。也许在 .NET 5.0 中......当然我们可以在任务本身中构建超时,但这并不好,这些东西必须免费。
  • 虽然您描述的两层超时仍然需要逻辑,但.NET 4.5 确实提供了一种简单的方法来创建基于超时的CancellationTokenSource。构造函数有两种重载,一种采用整数毫秒延迟,另一种采用 TimeSpan 延迟。
  • 完整的简单库源码在这里:stackoverflow.com/questions/11831844/…
  • 有完整源代码工作的最终解决方案吗?可能更复杂的示例用于通知每个线程中的错误,并且在 WaitAll 显示摘要之后?
  • 要添加到@patridge 建议的内容,也可以使用CancellationTokenSource.CancelAfter(<timespan or millisecs>)

标签: c# .net task-parallel-library


【解决方案1】:

这个怎么样:

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
    // task completed within timeout
} else { 
    // timeout logic
}

这里是a great blog post "Crafting a Task.TimeoutAfter Method" (from MS Parallel Library team) with more info on this sort of thing

补充:应对我的回答发表评论的要求,这是一个包括取消处理的扩展解决方案。请注意,将取消传递给任务和计时器意味着在您的代码中可以通过多种方式体验取消,您应该确保测试并确信您正确处理了所有这些。不要冒险尝试各种组合,并希望您的计算机在运行时做正确的事情。

int timeout = 1000;
var task = SomeOperationAsync(cancellationToken);
if (await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)) == task)
{
    // Task completed within timeout.
    // Consider that the task may have faulted or been canceled.
    // We re-await the task so that any exceptions/cancellation is rethrown.
    await task;

}
else
{
    // timeout/cancellation logic
}

【讨论】:

  • 需要说明的是,虽然Task.Delay可以在长时间运行的任务之前完成,让你可以处理超时的情况,但这并不会取消长时间运行的任务本身; WhenAny 只是让您知道传递给它的任务之一已经完成。您必须自己实现 CancellationToken 并取消长时间运行的任务。
  • 还可以注意到,Task.Delay 任务由系统计时器支持,无论SomeOperationAsync 花费多长时间,系统计时器都将继续跟踪该计时器,直到超时到期。因此,如果整个代码 sn-p 在一个紧密的循环中执行了很多,那么您正在为计时器消耗系统资源,直到它们全部超时。解决此问题的方法是将CancellationToken 传递给Task.Delay(timeout, cancellationToken),当SomeOperationAsync 完成以释放计时器资源时取消。
  • 取消代码的工作量太大了。试试这个: int timeout = 1000; var cancelTokenSource = new CancellationTokenSource(timeout); var cancelToken = tokenSource.Token; var task = SomeOperationAsync(cancellationToken);尝试{等待任务; // 此处添加代码成功完成 } catch (OperationCancelledException) { // 此处添加代码超时情况 }
  • @ilans 通过等待Task,任务存储的任何异常都会在此时重新抛出。这使您有机会捕获OperationCanceledException(如果取消)或任何其他异常(如果出现故障)。
  • @TomexOu:问题是如何异步等待任务的完成。 Task.Wait(timeout) 将同步阻塞而不是异步等待。
【解决方案2】:

这是一个扩展方法版本,它包含在原始任务完成时取消超时,正如 Andrew Arnott 在对his answer 的评论中所建议的那样。

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout) {

    using (var timeoutCancellationTokenSource = new CancellationTokenSource()) {

        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == task) {
            timeoutCancellationTokenSource.Cancel();
            return await task;  // Very important in order to propagate exceptions
        } else {
            throw new TimeoutException("The operation has timed out.");
        }
    }
}

【讨论】:

  • 给这个人一些选票。优雅的解决方案。如果你的调用没有返回类型,请确保你只删除 TResult。
  • CancellationTokenSource 是一次性的,应该在 using 块中
  • @It'satrap 两次等待任务只是在第二次等待时返回结果。它不会执行两次。你可以说它在执行两次时等于task.Result
  • 如果超时,原任务(task)还会继续运行吗?
  • 小改进机会:TimeoutException 有一个合适的默认消息。用“操作已超时”覆盖它。没有增加任何价值,实际上通过暗示有理由覆盖它而引起了一些混乱。
【解决方案3】:

您可以使用Task.WaitAny 等待多个任务中的第一个。

您可以创建两个额外的任务(在指定的超时后完成),然后使用WaitAny 等待先完成的任务。如果首先完成的任务是您的“工作”任务,那么您就完成了。如果最先完成的任务是超时任务,那么您可以对超时做出反应(例如请求取消)。

【讨论】:

  • 我见过我非常尊重的 MVP 使用的这种技术,它对我来说似乎比公认的答案要干净得多。也许一个例子将有助于获得更多选票!我会自愿这样做,除非我没有足够的任务经验来确信它会有所帮助:)
  • 一个线程会被阻塞——但如果你同意,那就没问题了。我采取的解决方案是下面的解决方案,因为没有线程被阻塞。我读了一篇非常好的博文。
  • @JJschk 你提到你采取了解决方案below .... 那是什么?基于 SO 排序?
  • 如果我不想取消较慢的任务怎么办?我想在它完成但从当前方法返回时处理它..
【解决方案4】:

这是先前答案的略微增强版本。

  • Lawrence's answer外,超时时取消原任务。
  • 除了sjb's answer variants 2 and 3,您还可以为原始任务提供CancellationToken,当超时发生时,您会得到TimeoutException而不是OperationCanceledException
async Task<TResult> CancelAfterAsync<TResult>(
    Func<CancellationToken, Task<TResult>> startTask,
    TimeSpan timeout, CancellationToken cancellationToken)
{
    using (var timeoutCancellation = new CancellationTokenSource())
    using (var combinedCancellation = CancellationTokenSource
        .CreateLinkedTokenSource(cancellationToken, timeoutCancellation.Token))
    {
        var originalTask = startTask(combinedCancellation.Token);
        var delayTask = Task.Delay(timeout, timeoutCancellation.Token);
        var completedTask = await Task.WhenAny(originalTask, delayTask);
        // Cancel timeout to stop either task:
        // - Either the original task completed, so we need to cancel the delay task.
        // - Or the timeout expired, so we need to cancel the original task.
        // Canceling will not affect a task, that is already completed.
        timeoutCancellation.Cancel();
        if (completedTask == originalTask)
        {
            // original task completed
            return await originalTask;
        }
        else
        {
            // timeout
            throw new TimeoutException();
        }
    }
}

用法

InnerCallAsync 可能需要很长时间才能完成。 CallAsync 用超时包裹它。

async Task<int> CallAsync(CancellationToken cancellationToken)
{
    var timeout = TimeSpan.FromMinutes(1);
    int result = await CancelAfterAsync(ct => InnerCallAsync(ct), timeout,
        cancellationToken);
    return result;
}

async Task<int> InnerCallAsync(CancellationToken cancellationToken)
{
    return 42;
}

【讨论】:

  • 感谢您的解决方案!似乎您应该将timeoutCancellation 传递给delayTask。目前,如果您取消取消,CancelAfterAsync 可能会抛出 TimeoutException 而不是 TaskCanceledException,因为 delayTask 可能会先完成。
  • @AxelUser,你是对的。我花了一个小时进行一系列单元测试来了解发生了什么:) 我假设当给WhenAny 的两个任务都被相同的原因取消时,WhenAny 将返回第一个任务。这个假设是错误的。我已经编辑了答案。谢谢!
  • 我很难弄清楚如何使用定义的 Task 函数实际调用它;你有没有机会举例说明如何调用它?
  • @jhaagsma,添加示例!
  • @JosefBláha 非常感谢!我仍然在慢慢地围绕 lambda 样式语法,这对我来说是没有想到的 - 通过传入 lambda 函数,令牌被传递到 CancelAfterAsync 的主体中的任务。漂亮!
【解决方案5】:

使用 Stephen Cleary 优秀的 AsyncEx 库,您可以:

TimeSpan timeout = TimeSpan.FromSeconds(10);

using (var cts = new CancellationTokenSource(timeout))
{
    await myTask.WaitAsync(cts.Token);
}

TaskCanceledException 将在超时的情况下被抛出。

【讨论】:

    【解决方案6】:

    这样的事情呢?

        const int x = 3000;
        const int y = 1000;
    
        static void Main(string[] args)
        {
            // Your scheduler
            TaskScheduler scheduler = TaskScheduler.Default;
    
            Task nonblockingTask = new Task(() =>
                {
                    CancellationTokenSource source = new CancellationTokenSource();
    
                    Task t1 = new Task(() =>
                        {
                            while (true)
                            {
                                // Do something
                                if (source.IsCancellationRequested)
                                    break;
                            }
                        }, source.Token);
    
                    t1.Start(scheduler);
    
                    // Wait for task 1
                    bool firstTimeout = t1.Wait(x);
    
                    if (!firstTimeout)
                    {
                        // If it hasn't finished at first timeout display message
                        Console.WriteLine("Message to user: the operation hasn't completed yet.");
    
                        bool secondTimeout = t1.Wait(y);
    
                        if (!secondTimeout)
                        {
                            source.Cancel();
                            Console.WriteLine("Operation stopped!");
                        }
                    }
                });
    
            nonblockingTask.Start();
            Console.WriteLine("Do whatever you want...");
            Console.ReadLine();
        }
    

    您可以使用 Task.Wait 选项,而无需使用另一个任务阻塞主线程。

    【讨论】:

    • 事实上,在这个例子中,你不是在 t1 内部等待,而是在一个上层任务上等待。我会尝试做一个更详细的例子。
    【解决方案7】:

    这是一个基于最高投票答案的完整示例,即:

    int timeout = 1000;
    var task = SomeOperationAsync();
    if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
        // task completed within timeout
    } else { 
        // timeout logic
    }
    

    这个答案中实现的主要优点是添加了泛型,因此函数(或任务)可以返回一个值。这意味着任何现有函数都可以包装在超时函数中,例如:

    之前:

    int x = MyFunc();
    

    之后:

    // Throws a TimeoutException if MyFunc takes more than 1 second
    int x = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));
    

    此代码需要 .NET 4.5。

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskTimeout
    {
        public static class Program
        {
            /// <summary>
            ///     Demo of how to wrap any function in a timeout.
            /// </summary>
            private static void Main(string[] args)
            {
    
                // Version without timeout.
                int a = MyFunc();
                Console.Write("Result: {0}\n", a);
                // Version with timeout.
                int b = TimeoutAfter(() => { return MyFunc(); },TimeSpan.FromSeconds(1));
                Console.Write("Result: {0}\n", b);
                // Version with timeout (short version that uses method groups). 
                int c = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));
                Console.Write("Result: {0}\n", c);
    
                // Version that lets you see what happens when a timeout occurs.
                try
                {               
                    int d = TimeoutAfter(
                        () =>
                        {
                            Thread.Sleep(TimeSpan.FromSeconds(123));
                            return 42;
                        },
                        TimeSpan.FromSeconds(1));
                    Console.Write("Result: {0}\n", d);
                }
                catch (TimeoutException e)
                {
                    Console.Write("Exception: {0}\n", e.Message);
                }
    
                // Version that works on tasks.
                var task = Task.Run(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    return 42;
                });
    
                // To use async/await, add "await" and remove "GetAwaiter().GetResult()".
                var result = task.TimeoutAfterAsync(TimeSpan.FromSeconds(2)).
                               GetAwaiter().GetResult();
    
                Console.Write("Result: {0}\n", result);
    
                Console.Write("[any key to exit]");
                Console.ReadKey();
            }
    
            public static int MyFunc()
            {
                return 42;
            }
    
            public static TResult TimeoutAfter<TResult>(
                this Func<TResult> func, TimeSpan timeout)
            {
                var task = Task.Run(func);
                return TimeoutAfterAsync(task, timeout).GetAwaiter().GetResult();
            }
    
            private static async Task<TResult> TimeoutAfterAsync<TResult>(
                this Task<TResult> task, TimeSpan timeout)
            {
                var result = await Task.WhenAny(task, Task.Delay(timeout));
                if (result == task)
                {
                    // Task completed within timeout.
                    return task.GetAwaiter().GetResult();
                }
                else
                {
                    // Task timed out.
                    throw new TimeoutException();
                }
            }
        }
    }
    

    注意事项

    给出这个答案后,通常不是在正常操作期间在代码中抛出异常是一个好习惯,除非你绝对必须:

    • 每次抛出异常,都是极其重量级的操作,
    • 如果异常处于紧密循环中,异常可能会使您的代码速度降低 100 倍或更多。

    仅当您绝对无法更改您正在调用的函数时才使用此代码,因此它会在特定的 TimeSpan 之后超时。

    这个答案实际上只适用于处理您根本无法重构以包含超时参数的第 3 方库时。

    如何编写健壮的代码

    如果你想编写健壮的代码,一般规则是这样的:

    每个可能无限期阻塞的操作都必须有一个超时时间。

    如果您遵守此规则,您的代码最终会遇到因某种原因而失败的操作,然后它将无限期阻塞,您的应用程序将永久挂起。

    如果一段时间后出现合理的超时,那么您的应用会挂起一段极端的时间(例如 30 秒),然后它会显示错误并继续愉快地进行,或者重试。

    【讨论】:

      【解决方案8】:

      从 .Net 6(Preview 7)或更高版本开始,有一个新的内置方法 Task.WaitAsync 来实现这一点。

      // Using TimeSpan
      await myTask.WaitAsync(TimeSpan.FromSeconds(10));
      
      // Using CancellationToken
      await myTask.WaitAsync(cancellationToken);
      
      // Using both TimeSpan and CancellationToken
      await myTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);
      

      【讨论】:

        【解决方案9】:

        使用Timer 处理消息并自动取消。任务完成后,在计时器上调用 Dispose 以便它们永远不会触发。这是一个例子;将 taskDelay 更改为 500、1500 或 2500 以查看不同的情况:

        using System;
        using System.Threading;
        using System.Threading.Tasks;
        
        namespace ConsoleApplication1
        {
            class Program
            {
                private static Task CreateTaskWithTimeout(
                    int xDelay, int yDelay, int taskDelay)
                {
                    var cts = new CancellationTokenSource();
                    var token = cts.Token;
                    var task = Task.Factory.StartNew(() =>
                    {
                        // Do some work, but fail if cancellation was requested
                        token.WaitHandle.WaitOne(taskDelay);
                        token.ThrowIfCancellationRequested();
                        Console.WriteLine("Task complete");
                    });
                    var messageTimer = new Timer(state =>
                    {
                        // Display message at first timeout
                        Console.WriteLine("X milliseconds elapsed");
                    }, null, xDelay, -1);
                    var cancelTimer = new Timer(state =>
                    {
                        // Display message and cancel task at second timeout
                        Console.WriteLine("Y milliseconds elapsed");
                        cts.Cancel();
                    }
                        , null, yDelay, -1);
                    task.ContinueWith(t =>
                    {
                        // Dispose the timers when the task completes
                        // This will prevent the message from being displayed
                        // if the task completes before the timeout
                        messageTimer.Dispose();
                        cancelTimer.Dispose();
                    });
                    return task;
                }
        
                static void Main(string[] args)
                {
                    var task = CreateTaskWithTimeout(1000, 2000, 2500);
                    // The task has been started and will display a message after
                    // one timeout and then cancel itself after the second
                    // You can add continuations to the task
                    // or wait for the result as needed
                    try
                    {
                        task.Wait();
                        Console.WriteLine("Done waiting for task");
                    }
                    catch (AggregateException ex)
                    {
                        Console.WriteLine("Error waiting for task:");
                        foreach (var e in ex.InnerExceptions)
                        {
                            Console.WriteLine(e);
                        }
                    }
                }
            }
        }
        

        此外,Async CTP 提供了一个 TaskEx.Delay 方法,该方法将为您将计时器包装在任务中。这可以让您有更多的控制权来做一些事情,比如在 Timer 触发时将 TaskScheduler 设置为继续。

        private static Task CreateTaskWithTimeout(
            int xDelay, int yDelay, int taskDelay)
        {
            var cts = new CancellationTokenSource();
            var token = cts.Token;
            var task = Task.Factory.StartNew(() =>
            {
                // Do some work, but fail if cancellation was requested
                token.WaitHandle.WaitOne(taskDelay);
                token.ThrowIfCancellationRequested();
                Console.WriteLine("Task complete");
            });
        
            var timerCts = new CancellationTokenSource();
        
            var messageTask = TaskEx.Delay(xDelay, timerCts.Token);
            messageTask.ContinueWith(t =>
            {
                // Display message at first timeout
                Console.WriteLine("X milliseconds elapsed");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
        
            var cancelTask = TaskEx.Delay(yDelay, timerCts.Token);
            cancelTask.ContinueWith(t =>
            {
                // Display message and cancel task at second timeout
                Console.WriteLine("Y milliseconds elapsed");
                cts.Cancel();
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
        
            task.ContinueWith(t =>
            {
                timerCts.Cancel();
            });
        
            return task;
        }
        

        【讨论】:

        • 他不希望当前线程被阻塞,也就是没有task.Wait()
        • @Danny:这只是为了使示例完整。在 ContinueWith 之后,您可以返回并让任务运行。我会更新我的答案以使其更清楚。
        • @dtb:如果您将 t1 设为 Task> 然后调用 TaskExtensions.Unwrap 会怎样?你可以从你的内部 lambda 返回 t2,然后你可以在解包后的任务中添加延续。
        • 太棒了!这完美地解决了我的问题。谢谢!我想我会采用@AS-CII 提出的解决方案,尽管我希望我也能接受你对 TaskExtensions 建议的回答。我应该打开一个新问题,这样你就可以获得你应得的代表吗?
        【解决方案10】:

        解决此问题的另一种方法是使用响应式扩展:

        public static Task TimeoutAfter(this Task task, TimeSpan timeout, IScheduler scheduler)
        {
                return task.ToObservable().Timeout(timeout, scheduler).ToTask();
        }
        

        在你的单元测试中使用下面的代码进行测试,它对我有用

        TestScheduler scheduler = new TestScheduler();
        Task task = Task.Run(() =>
                        {
                            int i = 0;
                            while (i < 5)
                            {
                                Console.WriteLine(i);
                                i++;
                                Thread.Sleep(1000);
                            }
                        })
                        .TimeoutAfter(TimeSpan.FromSeconds(5), scheduler)
                        .ContinueWith(t => { }, TaskContinuationOptions.OnlyOnFaulted);
        
        scheduler.AdvanceBy(TimeSpan.FromSeconds(6).Ticks);
        

        您可能需要以下命名空间:

        using System.Threading.Tasks;
        using System.Reactive.Subjects;
        using System.Reactive.Linq;
        using System.Reactive.Threading.Tasks;
        using Microsoft.Reactive.Testing;
        using System.Threading;
        using System.Reactive.Concurrency;
        

        【讨论】:

          【解决方案11】:

          上面@Kevan 答案的通用版本,使用响应式扩展。

          public static Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout, IScheduler scheduler)
          {
              return task.ToObservable().Timeout(timeout, scheduler).ToTask();
          }
          

          带有可选的调度器:

          public static Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout, Scheduler scheduler = null)
          {
              return scheduler is null 
                 ? task.ToObservable().Timeout(timeout).ToTask() 
                 : task.ToObservable().Timeout(timeout, scheduler).ToTask();
          }
          

          BTW:当发生超时时,会抛出超时异常

          【讨论】:

            【解决方案12】:

            使用 .Net 6(预览版 7 作为此答案的日期),可以使用新的 WaitAsync(TimeSpan, CancellationToken) 来满足这一特殊需求。 如果您可以使用 .Net6,如果我们与本文中提出的大多数好的解决方案进行比较,该版本还被描述为进行了优化。

            (感谢所有参与者,因为我多年来一直使用您的解决方案)

            【讨论】:

              【解决方案13】:

              我觉得Task.Delay() 任务和CancellationTokenSource 在其他答案中对于我在紧密网络循环中的用例来说有点多。

              虽然Joe Hoag's Crafting a Task.TimeoutAfter Method on MSDN blogs 很鼓舞人心,但出于与上述相同的原因,我有点厌倦了使用TimeoutException 进行流量控制,因为预计会更频繁地发生超时。

              所以我选择了这个,它也处理了博客中提到的优化:

              public static async Task<bool> BeforeTimeout(this Task task, int millisecondsTimeout)
              {
                  if (task.IsCompleted) return true;
                  if (millisecondsTimeout == 0) return false;
              
                  if (millisecondsTimeout == Timeout.Infinite)
                  {
                      await Task.WhenAll(task);
                      return true;
                  }
              
                  var tcs = new TaskCompletionSource<object>();
              
                  using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs,
                      millisecondsTimeout, Timeout.Infinite))
                  {
                      return await Task.WhenAny(task, tcs.Task) == task;
                  }
              }
              

              一个示例用例是这样的:

              var receivingTask = conn.ReceiveAsync(ct);
              
              while (!await receivingTask.BeforeTimeout(keepAliveMilliseconds))
              {
                  // Send keep-alive
              }
              
              // Read and do something with data
              var data = await receivingTask;
              

              【讨论】:

                【解决方案14】:

                Andrew Arnott 回答的一些变体:

                1. 如果您想等待现有任务并了解它是否已完成或超时,但又不想在发生超时时取消它:

                  public static async Task<bool> TimedOutAsync(this Task task, int timeoutMilliseconds)
                  {
                      if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
                  
                      if (timeoutMilliseconds == 0) {
                          return !task.IsCompleted; // timed out if not completed
                      }
                      var cts = new CancellationTokenSource();
                      if (await Task.WhenAny( task, Task.Delay(timeoutMilliseconds, cts.Token)) == task) {
                          cts.Cancel(); // task completed, get rid of timer
                          await task; // test for exceptions or task cancellation
                          return false; // did not timeout
                      } else {
                          return true; // did timeout
                      }
                  }
                  
                2. 如果你想启动一个工作任务,如果超时就取消这个工作:

                  public static async Task<T> CancelAfterAsync<T>( this Func<CancellationToken,Task<T>> actionAsync, int timeoutMilliseconds)
                  {
                      if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
                  
                      var taskCts = new CancellationTokenSource();
                      var timerCts = new CancellationTokenSource();
                      Task<T> task = actionAsync(taskCts.Token);
                      if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
                          timerCts.Cancel(); // task completed, get rid of timer
                      } else {
                          taskCts.Cancel(); // timer completed, get rid of task
                      }
                      return await task; // test for exceptions or task cancellation
                  }
                  
                3. 如果您已经创建了一个任务,如果发生超时要取消:

                  public static async Task<T> CancelAfterAsync<T>(this Task<T> task, int timeoutMilliseconds, CancellationTokenSource taskCts)
                  {
                      if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
                  
                      var timerCts = new CancellationTokenSource();
                      if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
                          timerCts.Cancel(); // task completed, get rid of timer
                      } else {
                          taskCts.Cancel(); // timer completed, get rid of task
                      }
                      return await task; // test for exceptions or task cancellation
                  }
                  

                另外说明,这些版本如果没有超时就会取消定时器,所以多次调用不会导致定时器堆积。

                sjb

                【讨论】:

                  【解决方案15】:

                  所以这是古老的,但有一个更好的现代解决方案。不确定需要什么版本的 c#/.NET,但我就是这样做的:

                  
                  ... Other method code not relevant to the question.
                  
                  // a token source that will timeout at the specified interval, or if cancelled outside of this scope
                  using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                  using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutTokenSource.Token);
                  
                  async Task<MessageResource> FetchAsync()
                  {
                      try
                      {
                          return await MessageResource.FetchAsync(m.Sid);
                      } catch (TaskCanceledException e)
                      {
                          if (timeoutTokenSource.IsCancellationRequested)
                              throw new TimeoutException("Timeout", e);
                          throw;
                      }
                  }
                  
                  return await Task.Run(FetchAsync, linkedTokenSource.Token);
                  

                  CancellationTokenSource 构造函数采用TimeSpan 参数,该参数将导致该令牌在该间隔过去后取消。然后,您可以将您的异步(或同步)代码包装在另一个对 Task.Run 的调用中,并传递超时令牌。

                  这假设您正在传递一个取消令牌(token 变量)。如果不需要和超时分开取消任务,直接使用timeoutTokenSource即可。否则,您创建linkedTokenSource,如果发生超时,它将取消,如果它被取消。

                  然后我们只捕获OperationCancelledException 并检查哪个令牌引发了异常,如果超时导致引发此异常,则抛出TimeoutException。否则,我们重新抛出。

                  另外,我在这里使用本地函数,这是在 C# 7 中引入的,但您可以轻松地使用 lambda 或实际函数来实现相同的效果。同样,c# 8 引入了一种更简单的 using 语句语法,但这些语句很容易重写。

                  【讨论】:

                    【解决方案16】:

                    创建一个扩展以等待任务或延迟完成,以先到者为准。如果延迟获胜,则抛出异常。

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

                    【讨论】:

                      【解决方案17】:

                      为了好玩,我对 Task 做了一个“OnTimeout”扩展。超时任务执行所需的内联 lambda Action() 并返回 true,否则返回 false。

                      public static async Task<bool> OnTimeout<T>(this T t, Action<T> action, int waitms) where T : Task
                      {
                          if (!(await Task.WhenAny(t, Task.Delay(waitms)) == t))
                          {
                              action(t);
                              return true;
                          } else {
                              return false;
                          }
                      }
                      

                      OnTimeout 扩展返回一个 bool 结果,可以将其分配给一个变量,如本例中调用 UDP 套接字异步:

                      var t = UdpSocket.ReceiveAsync();
                      
                      var timeout = await t.OnTimeout(task => {
                          Console.WriteLine("No Response");
                      }, 5000);
                      

                      “任务”变量可在超时 lambda 中访问以进行更多处理。

                      使用接收对象的 Action 可能会激发其他各种扩展设计。

                      【讨论】:

                        【解决方案18】:

                        如果您使用 BlockingCollection 来安排任务,生产者可以运行可能需要长时间运行的任务,而消费者可以使用内置超时和取消令牌的 TryTake 方法。

                        【讨论】:

                        • 我必须写一些东西(不想在这里放专有代码),但场景是这样的。生产者将是执行可能超时的方法的代码,并在完成后将结果放入队列中。消费者将在超时时调用 trytake() 并在超时时接收令牌。生产者和消费者都将是后台任务,并在需要时使用 UI 线程调度程序向用户显示消息。
                        【解决方案19】:

                        我在这里将一些其他答案的想法和this answer on another thread 重新组合成一个 Try 样式的扩展方法。如果您想要一个扩展方法,这有一个好处,但可以避免超时时出现异常。

                        public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task,
                            TimeSpan timeout, Action<TResult> successor)
                        {
                        
                            using var timeoutCancellationTokenSource = new CancellationTokenSource();
                            var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token))
                                                          .ConfigureAwait(continueOnCapturedContext: false);
                        
                            if (completedTask == task)
                            {
                                timeoutCancellationTokenSource.Cancel();
                        
                                // propagate exception rather than AggregateException, if calling task.Result.
                                var result = await task.ConfigureAwait(continueOnCapturedContext: false);
                                successor(result);
                                return true;
                            }
                            else return false;        
                        }     
                        
                        async Task Example(Task<string> task)
                        {
                            string result = null;
                            if (await task.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), r => result = r))
                            {
                                Console.WriteLine(result);
                            }
                        }    
                        

                        【讨论】:

                          【解决方案20】:

                          这是WaitAsync 方法的低级实现,它接受超时和CancellationToken,并且在发生异常时传播Task&lt;T&gt; 的所有错误,而不仅仅是第一个错误:

                          public static Task<TResult> WaitAsync<TResult>(this Task<TResult> task,
                              TimeSpan timeout, CancellationToken cancellationToken = default)
                          {
                              if (task == null) throw new ArgumentNullException(nameof(task));
                              if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
                                  throw new ArgumentOutOfRangeException(nameof(timeout));
                          
                              var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                              cts.CancelAfter(timeout);
                          
                              return task
                                  .ContinueWith(_ => { }, cts.Token,
                                      TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
                                  .ContinueWith(continuation =>
                                  {
                                      cts.Dispose();
                                      if (task.IsCompleted) return task;
                                      cancellationToken.ThrowIfCancellationRequested();
                                      if (continuation.IsCanceled) throw new TimeoutException();
                                      return task;
                                  }, TaskScheduler.Default).Unwrap();
                          }
                          

                          如果在task 完成之前超时,则抛出TimeoutException

                          在这种情况下,诚实地传播所有错误并不是真正的增值功能。原因是,如果你像这样使用WaitAsyncawait someTask.WaitAsync(timeout),任何额外的错误都会被await 操作符吞下,这在设计上只会传播等待任务的第一个异常。将WaitAsync 任务存储在一个变量中并在catch 块中检查它没有多大意义,因为您已经拥有可用的someTask,您可以改为检查它。

                          【讨论】:

                            猜你喜欢
                            • 2020-10-04
                            • 2017-12-18
                            • 1970-01-01
                            • 1970-01-01
                            • 2019-05-31
                            • 2016-09-04
                            • 1970-01-01
                            相关资源
                            最近更新 更多