我希望等待一个特定规则的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
}
这里有一个 来自MS Parallel Library团队的绝佳博客文章“Crafting a Task.TimeoutAfter Method”,提供了更多关于这类问题的信息。
附加:根据我的回答评论请求,这里提供了一个包括取消处理的扩展解决方案。注意,将取消操作传递给任务和定时器意味着您的代码中可能存在多种取消方式,您应该确保测试并自信地处理所有这些情况。不要让各种组合只靠计算机运行时的正确判断而留给机会。
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
任务由系统计时器支持,无论 SomeOperationAsync
花费多长时间,直到超时到期,计时器都将继续跟踪。因此,如果这个代码片段在紧密循环中频繁执行,你会消耗系统资源,直到所有计时器超时。解决这个问题的方法是使用 CancellationToken
,将其传递给 Task.Delay(timeout, cancellationToken)
,并在 SomeOperationAsync
完成时取消它以释放计时器资源。 - Andrew ArnottTask.Wait(timeout)
会同步阻塞,而不是异步等待。 - Andrew Arnott以下是一个扩展方法版本,它包含了在原始任务完成时取消超时的功能,正如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.");
}
}
}
using
代码块中以确保正确释放。翻译完成,没有返回额外的内容。 - PeterMtask.Result
。 - M. Mimpentask
)是否仍会继续运行? - jagTimeoutException
已经有一个合适的默认消息。将其覆盖为“操作已超时”没有增加任何价值,实际上会造成一些困惑,因为这暗示着需要覆盖它的原因。 - Edward Brey从 .Net 6(预览版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);
TimeSpan
或CancellationToken
之前没有完成,则会分别抛出TimeoutException
或TaskCanceledException
。try
{
await myTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);
}
catch (TaskCanceledException)
{
Console.WriteLine("Task didn't get finished before the `CancellationToken`");
}
catch (TimeoutException)
{
Console.WriteLine("Task didn't get finished before the `TimeSpan`");
}
您可以使用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;
}
WhenAny
给出的两个任务都被同一个标记取消时,WhenAny
将返回第一个任务。这个假设是错误的。我已经编辑了答案。谢谢! - Josef Bláha使用Stephen Cleary优秀的AsyncEx库,你可以这样做:
TimeSpan timeout = TimeSpan.FromSeconds(10);
using (var cts = new CancellationTokenSource(timeout))
{
await myTask.WaitAsync(cts.Token);
}
如果超时,将抛出TaskCanceledException
异常。
这里是一个完整的示例,基于得票最高的答案:
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
后超时时才使用此代码。
此答案仅适用于处理第三方库,而你无法重构该库以包含超时参数。
如何编写稳健的代码
如果你想编写稳健的代码,一般规则如下:
每个潜在可能无限期阻塞的操作都必须有一个超时时间。
如果你不遵守这个规则,你的代码最终会碰到某些原因失败的操作,然后它将无限期地阻塞,你的应用程序就会永久挂起。
如果在一段时间后设置了合理的超时时间,那么你的应用程序将挂起一段极长的时间(例如30秒),然后它要么显示错误并继续执行,要么重试。
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();
}
使用计时器来处理消息和自动取消。当任务完成时,在计时器上调用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);
}
}
}
}
}
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()
。 - Cheng Chen随着 .Net 6(预览版7)的推出,现在可以使用新的WaitAsync(TimeSpan, CancellationToken)来满足这个特定需求。
如果您能使用 .Net6,这个版本还被描述为优化了大部分在此帖中提出的好解决方案。
(感谢所有参与者,因为我用了你们的解决方案多年)