我想等待一个任务<T>完成一些特殊的规则: 如果在X毫秒后还没有完成,我希望向用户显示一条消息。 如果在Y毫秒后还没有完成,我想自动请求取消。
我可以使用Task。ContinueWith异步等待任务完成(即计划在任务完成时执行一个操作),但不允许指定超时。 我可以使用Task。等待同步等待任务超时完成,但这会阻塞我的线程。 我如何异步等待任务超时完成?
我想等待一个任务<T>完成一些特殊的规则: 如果在X毫秒后还没有完成,我希望向用户显示一条消息。 如果在Y毫秒后还没有完成,我想自动请求取消。
我可以使用Task。ContinueWith异步等待任务完成(即计划在任务完成时执行一个操作),但不允许指定超时。 我可以使用Task。等待同步等待任务超时完成,但这会阻塞我的线程。 我如何异步等待任务超时完成?
当前回答
我将这里的一些其他答案和另一个线程上的这个答案重新组合成一个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);
}
}
其他回答
下面是一个基于投票最多的答案的完整示例,即:
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之后超时。
这个答案实际上只适用于处理无法重构为包含超时参数的第三方库库。
如何编写健壮的代码
如果你想写健壮的代码,一般规则是这样的:
每一个可能无限期阻塞的操作都必须有一个超时。
如果你不遵守这条规则,你的代码最终会遇到一个操作因为某种原因失败,然后它会无限期地阻塞,你的应用程序就会永久挂起。
如果在一段时间后出现了合理的超时,那么你的应用程序会挂起一段极端的时间(例如30秒),然后它会显示一个错误并继续它的快乐方式,或者重试。
安德鲁·阿诺特(Andrew Arnott)回答的几个变体:
If you want to wait for an existing task and find out whether it completed or timed out, but don't want to cancel it if the timeout occurs: 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 } } If you want to start a work task and cancel the work if the timeout occurs: 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 } If you have a task already created that you want to cancel if a timeout occurs: 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
像这样的东西怎么样?
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。WaitAny用于等待多个任务中的第一个。
您可以创建两个额外的任务(在指定的超时后完成),然后使用WaitAny等待先完成的任务。如果最先完成的任务是你的“工作”任务,那么你就完成了。如果最先完成的任务是一个超时任务,那么您可以对超时做出反应(例如,请求取消)。
为了它的乐趣,我做了一个“OnTimeout”扩展任务。超时时Task执行所需的内联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);
在timeout lambda中可以访问“task”变量以进行更多处理。
Action接收对象的使用可能会启发其他各种扩展设计。