一、使用 Task.WaitAll
public static async Task<string> GetDataAsync()
{
var data = await GetData();
return data;
}
private static Task<string> GetData()
{
var task = new Task<string>(() =>
{
Thread.Sleep(1000);
return "hello";
});
task.Start(); //开始运行
//判断超时
var completed = Task.WaitAll(new Task[]{ task }, TimeSpan.FromSeconds(3));
if (!completed)
{
throw new Exception("timeout");
}
return task;
}
Task.WaitAll 有个缺点,它是同步的,会阻塞当前 或 UI 线程。
二、使用 Task.WhenAny 和 CancellationTokenSource
Task.WhenAny返回最先完成的任务对象,CancellationTokenSource可以用来取消未完成任务。
public static async Task<string> GetDataAsync()
{
var data = await GetData(5);
return data;
}
private static async Task<string> GetData(int timeout)
{
//工作任务
var workTask = Task.Run(() =>
{
Thread.Sleep(20000);
return "hello";
});
using (var cts = new CancellationTokenSource())
{
//工作任务 + 超时任务 哪个先完成
var completedTask = await Task.WhenAny(workTask, Task.Delay(timeout * 1000, cts.Token));
//如果工作任务先完成
if (completedTask == workTask)
{
cts.Cancel(); //取消 Task.Delay 任务
return await workTask;
}
else
{
//否则超时
throw new TimeoutException("The operation has timed out.");
}
}
}
这里借助了 Task.Delay做了任务执行时间对比,如果工作任务晚于超时任务完成,即工作超时。
但是这样写会有一个问题,就是每次做超时都会写这段代码,可以考虑改为扩展方法。
Task无返回值
public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
if (completedTask == task)
{
timeoutCancellationTokenSource.Cancel();
await task;
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}
}
Task有返回值
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;
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}
}
测试代码
public static async Task<string> GetDataAsync()
{
var data = await GetData(5);
return data;
}
private static async Task<string> GetData(int timeout)
{
//工作任务
var workTask = Task.Run(() =>
{
Thread.Sleep(2000);
return "hello";
});
//5秒后超时
return await workTask.TimeoutAfter(TimeSpan.FromSeconds(5));
}
但是这样还有一个问题,虽然能检测到超时,但 workTask 任务还是会继续执行到完毕。要想超时就取消操作,这就要为 workTask 也设置 CancellationTokenSource。
三、仅使用 CancellationTokenSource 处理超时
虽然CancellationTokenSource只是管理取消任务的令牌,本身没有超时检测的能力。
但是我们可以利用它的延迟功能达到: 延迟一定的时间后(认为超时了)取消任务。
先看下面的代码,是永远不会检测到超时的
public static async Task<string> GetDataAsync()
{
var data = await GetData(5);
return data;
}
private static async Task<string> GetData(int timeout)
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)))
{
//工作任务
var workTask = Task.Run(() =>
{
Thread.Sleep(8000);
return "hello";
}, cts.Token);
return await workTask;
}
}
原因是,我们只是延迟 5 秒来让 CancellationTokenSource 取消操作,虽然它被用到 Task 第二个参数里,但是我们的工作内容里并没有处理取消操作的代码,只用了 Thread.Sleep(8000); 这种同步方法不支持取消,任务还是会一直执行下去。
Task 任务要能被取消,就要在工作内容中使用支持取消操作的代码,例如IsCancellationRequested 和 ThrowIfCancellationRequested()。
//工作任务
var workTask = Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
if (cts.Token.IsCancellationRequested)
{
return "timeout";
}
Thread.Sleep(1000);
}
return "hello";
}, cts.Token);
//工作任务
var workTask = Task.Run(() =>
{
try
{
for (int i = 0; i < 10; i++)
{
cts.Token.ThrowIfCancellationRequested();
Thread.Sleep(1000);
}
}
catch (Exception e)
{
return "timeout";
}
return "hello";
}, cts.Token);
下面这段代码是核心,它能延迟 timeout 秒后,自动执行 Cancel() 命令。
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout))
通过 CancellationTokenSource 的延迟取消功能,就能为 Task 赋予超时检测能力。
四、TaskCompletionSource 处理超时
很多时候,Task 是结合 TaskCompletionSource 使用的,本质上还是处理Task 的超时,只不过我们要多处理一下TaskCompletionSource 的结果。可以使用 TaskCompletionSource 的 SetException 或 SetCanceled 来处理。
实现方式:还是使用Task.WhenAny(看谁先完成) + CancellationTokenSource(取消未完成的任务)。
public static async Task<string> GetDataAsync()
{
var data = await GetData(5);
return data;
}
private static async Task<string> GetData(int timeout)
{
var tcs = new TaskCompletionSource<string>();
using (var cts = new CancellationTokenSource())
{
var workTask = Task.Run(() =>
{
//模拟耗时
for (int i = 0; i < 10; i++)
{
if (cts.IsCancellationRequested)
{
//tcs.SetCanceled(); //设置了 SetException 就可以不用设置 SetCanceled
return;
}
Thread.Sleep(1000);
}
tcs.SetResult("工作完成"); //正常执行完成
}, cts.Token);
var completedTask = await Task.WhenAny(workTask, Task.Delay(TimeSpan.FromSeconds(timeout), cts.Token));
if (completedTask == workTask)
{
cts.Cancel(); //取消 Task.Delay 操作
}
else
{
cts.Cancel(); //取消 workTask 操作
tcs.SetException(new TimeoutException("超时啦")); //设置超时异常
}
}
return await tcs.Task;
}
未超时 SetResult,超时后 SetException() 或 SetCanceled()。
如果不清楚何时会触发 SetResult 方法,可以直接比较 tcs.Task 和 Task.Delay
public static async Task<string> GetDataAsync()
{
var data = await GetData(5);
return data;
}
private static async Task<string> GetData(int timeout)
{
var tcs = new TaskCompletionSource<string>();
//模拟外部耗时
Task.Run(() =>
{
Thread.Sleep(4000);
tcs.SetResult("完成");
});
using (var cts = new CancellationTokenSource())
{
var completed = await Task.WhenAny(tcs.Task, Task.Delay(3000, cts.Token));
if (completed == tcs.Task)
{
cts.Cancel();
return await tcs.Task;
}
else
{
throw new Exception("timeout");
}
}
}

被折叠的 条评论
为什么被折叠?



