我想通过一些特殊规则等待Task<T>完成:如果 X 毫秒后仍未完成,我想向用户显示一条消息。如果 Y 毫秒后还没有完成,我想自动请求取消。
我可以使用Task.ContinueWith异步等待任务完成(即安排在任务完成时执行的操作),但这不允许指定超时。我可以使用Task.Wait同步等待任务超时完成,但这会阻塞我的线程。如何异步等待任务超时完成?
我想通过一些特殊规则等待Task<T>完成:如果 X 毫秒后仍未完成,我想向用户显示一条消息。如果 Y 毫秒后还没有完成,我想自动请求取消。
我可以使用Task.ContinueWith异步等待任务完成(即安排在任务完成时执行的操作),但这不允许指定超时。我可以使用Task.Wait同步等待任务超时完成,但这会阻塞我的线程。如何异步等待任务超时完成?
这个怎么样:
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 团队),其中包含有关此类事情的更多信息。
另外:应对我的回答发表评论的要求,这是一个扩展的解决方案,其中包括取消处理。请注意,将取消传递给任务和计时器意味着在您的代码中可以通过多种方式体验取消,您应该确保测试并确信您正确处理了所有这些。不要冒险进行各种组合,并希望您的计算机在运行时做正确的事情。
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 在对他的回答的评论中所建议的那样。
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
等待多个任务中的第一个。
您可以创建两个额外的任务(在指定的超时后完成),然后用于WaitAny
等待先完成的任务。如果首先完成的任务是您的“工作”任务,那么您就完成了。如果最先完成的任务是超时任务,那么您可以对超时做出反应(例如请求取消)。
这是先前答案的略微增强版本。
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;
}
使用 Stephen Cleary 优秀的AsyncEx库,您可以:
TimeSpan timeout = TimeSpan.FromSeconds(10);
using (var cts = new CancellationTokenSource(timeout))
{
await myTask.WaitAsync(cts.Token);
}
TaskCanceledException
超时时会抛出。
这样的事情呢?
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
}
此答案中实现的主要优点是添加了泛型,因此函数(或任务)可以返回一个值。这意味着任何现有函数都可以包装在超时函数中,例如:
前:
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();
}
}
}
}
注意事项
给出这个答案后,在正常操作期间在代码中抛出异常通常不是一个好习惯,除非您绝对必须:
仅当您绝对无法更改您正在调用的函数时才使用此代码,因此它在特定的TimeSpan
.
此答案仅在处理您根本无法重构以包含超时参数的第 3 方库时才适用。
如何编写健壮的代码
如果你想编写健壮的代码,一般规则是这样的:
每个可能无限期阻塞的操作都必须有一个超时。
如果你不遵守这条规则,你的代码最终会遇到一个由于某种原因而失败的操作,然后它会无限期地阻塞,你的应用程序就会永久挂起。
如果一段时间后出现合理的超时,那么您的应用程序将挂起一段极端的时间(例如 30 秒),然后它会显示错误并继续以愉快的方式继续,或者重试。
从 .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);
使用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 方法,它将为您将计时器包装在任务中。这可以让您更好地控制执行诸如设置 TaskScheduler 以在 Timer 触发时继续执行之类的操作。
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();
}
在你的单元测试中使用下面的代码进行测试,它对我有用
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;
上面@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:当发生超时时,将抛出超时异常
使用 .Net 6(预览 7 作为此答案的日期),可以使用新的WaitAsync(TimeSpan, CancellationToken)来满足这一特殊需求。如果您可以使用 .Net6,如果我们与本文中提出的大多数好的解决方案进行比较,该版本还被描述为进行了优化。
(感谢所有参与者,因为我多年来一直使用您的解决方案)
对于我在紧密网络循环中的用例,我觉得Task.Delay()
任务和其他答案有点多。CancellationTokenSource
尽管Joe Hoag 在 MSDN 博客上的 Crafting a Task.TimeoutAfter Method很鼓舞人心,但出于与上述相同的原因,我有点厌倦了使用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;
Andrew Arnott 回答的一些变体:
如果您想等待现有任务并了解它是否已完成或超时,但又不想在发生超时时取消它:
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
}
}
如果要启动一个工作任务,如果发生超时则取消该工作:
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
}
如果您已经创建了要在发生超时时取消的任务:
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
所以这是古老的,但有一个更好的现代解决方案。不确定需要什么版本的 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 语句语法,但这些语法很容易重写。
创建扩展以等待任务或延迟完成,以先到者为准。如果延迟获胜,则抛出异常。
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;
}
为了好玩,我对 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 套接字 Async 的示例:
var t = UdpSocket.ReceiveAsync();
var timeout = await t.OnTimeout(task => {
Console.WriteLine("No Response");
}, 5000);
'task' 变量可在超时 lambda 中访问以进行更多处理。
接收对象的 Action 的使用可能会激发各种其他扩展设计。
如果您使用 BlockingCollection 来安排任务,生产者可以运行可能长时间运行的任务,而消费者可以使用内置超时和取消令牌的 TryTake 方法。
我在这里将其他一些答案的想法和另一个线程上的这个答案重新组合成一个 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);
}
}
WaitAsync
这是一个接受 timeout 和 a 的方法的低级实现CancellationToken
,并且在发生异常的情况下会传播 的所有错误Task<T>
,而不仅仅是第一个错误:
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();
}
TimeoutException
如果在完成之前超时,则task
抛出A。
在这种情况下,诚实地传播所有错误并不是真正的增值功能。原因是如果你使用WaitAsync
这样的: await someTask.WaitAsync(timeout)
,任何额外的错误都会被await
操作员吞下,这在设计上只会传播等待任务的第一个异常。WaitAsync
并且将任务存储在变量中并在块内检查它没有多大意义catch
,因为您已经有了someTask
可用的,您可以改为检查它。