asynchronous等待Task <T>完成超时

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

我可以使用Task.ContinueWithasynchronous等待任务完成(例如,计划任务完成时要执行的操作),但是不允许指定超时。 我可以使用Task.Wait同步等待任务完成超时,但是阻止我的线程。 我如何asynchronous地等待任务完成超时?

这个怎么样:

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

这里有一个很棒的博客文章“Crafting a Task.TimeoutAfter Method”(来自MS Parallel Library团队),有关这方面的更多信息 。

另外 :根据对我的回答的评论请求,这是一个扩展的解决scheme,包括取消处理。 请注意,将取消传递给任务和计时器意味着在您的代码中可以有多种取消方式,您应该确保testing并自信地处理所有这些方法。 不要随意组合各种组合,并希望你的电脑在运行时做正确的事情。

 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 } 

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

 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."); } } } 

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

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

这样的事情呢?

  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选项,而不使用其他任务阻塞主线程。

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

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

在这个答案的实现的主要优点是generics已被添加,所以函数(或任务)可以返回一个值。 这意味着任何现有的函数都可以用超时函数封装,例如:

之前:

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

注意事项

已经给出了这个答案,在正常的操作中,在你的代码中抛出exception通常不是一个好习惯,除非你绝对必须:

  • 每次抛出一个exception,它的一个非常重量级的操作,
  • 如果exception处于紧密的循环中,exception会使代码减慢100倍或更多。

如果你绝对不能改变你正在调用的函数,那么只有使用这个代码,以便在特定的TimeSpan之后超时。

这个答案实际上只适用于处理第三方库库,你根本不能重构包含超时参数。

如何编写健壮的代码

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

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

如果你没有遵守这个规则,你的代码最终会碰到一个失败的操作,然后它会无限期地被阻止,你的应用程序就会永久挂起。

如果在一段时间之后有合理的超时,那么你的应用程序会挂起一段极端的时间(例如30秒),然后它会显示一个错误并继续其快乐的方式,或者重试。

使用计时器处理消息并自动取消。 任务完成后,调用定时器上的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方法,将定时器包装在你的任务中。 这可以给你更多的控制来做一些事情,比如设置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; } 

解决这个问题的另一种方法是使用Reactive Extensions:

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

在你的unit testing中使用下面的代码进行testing,它适用于我

 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; 

使用Stephen Cleary出色的AsyncEx库,您可以:

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

如果发生超时, TaskCanceledException将被抛出。

以上是带有Reactive Extensions的@ 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 == null ? task.ToObservable().Timeout(timeout).ToTask() : task.ToObservable().Timeout(timeout, scheduler).ToTask(); } 

顺便说一句:超时发生时,超时exception将被抛出

如果您使用BlockingCollection来计划任务,则生产者可以运行潜在的长时间运行任务,并且使用者可以使用内置超时和取消令牌的TryTake方法。