让Task支持带超时的非阻塞异步等待

前言

大家都知道Task表示一个异步任务。如果我们想等待一个Task完成,有很多自带的实例、静态方法供我们选择。有的阻塞,有的不阻塞。不过带超时的等待只有一个,而且它是堵塞的。

这次给大家写个非阻塞的带超时的等待方法~ # Task已有的等待方法

Task实例已有的等待方法就是Wait ▲ 五个重载,一个无限等待,一个支持取消,两个支持超时(毫秒和TimeSpan),一个既支持取消也支持超时

但Task实例的等待方法的所有重载都有一个弊端,那就是阻塞。如果真的用这个方法来等待这个Task,那么一定会堵塞一个线程。所以通常不建议这样直接等待。

另外,Task还提供了静态的等待方法: ▲ Task静态的等待方法

Task.WaitAllTask.WaitAny的功能基本和Task实例的Wait方法是一样的,只是可以等待多个Task的实例罢了。

Task.WhenAllWhenAny才是不阻塞线程的异步等待。

可是!可以看上图,只有Task.Wait系列的重载才有timout参数,也就是超时功能,而Task.When系列则没有。

这也就是本文要讨论和解决的问题。如何让一个Task的等待既可以有超时功能,又是异步的不堵塞线程呢?

带超时的异步非阻塞等待方法的实现

Task有一个Task.Delay静态方法,它是用来创建一个在指定时间延迟后完成的任务,类似于一个异步的Thread.Sleep。而我们就可以用这个方法来间接实现异步非阻塞的等待。

Task.WhenAny可以异步的等待多个任务中任意一个任务的完成。这样就可以和Task.Delay做一个结合。思路就是要么是真正在执行的任务先完成,要么就是超时先完成。于是我们可以用Task.Delay来创建一个新的Task,来比较两个Task的执行先后:

1
2
3
4
5
6
7
8
9
10
11
public static async Task<TResult> WaitAsync<TResult>(Task<TResult> task, TimeSpan timeout)
{
if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
{
return await task;
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}

但这个扩展封装还不够好,如果我们的任务早已完成,但timeout设置的过长,那么Task.Delay创建的延时任务会一直等到timeout设置的时间结束后才结束,会一直常驻后台,占用资源。

还好延时任务可以被取消,于是我们可以用CancellationTokenSource,把无用的延时任务给释放掉。

然后就有了以下完整的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
namespace AmazingPP
{
public static class TaskWaitingExtensions
{
public static async Task WaitAsync(this Task task,TimeSpan timeout)
{
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var delayTask = Task.Delay(timeout, timeoutCancellationTokenSource.Token);
if (await Task.WhenAny(task,delayTask) == task)
{
timeoutCancellationTokenSource.Cancel();
await task;
}
else
{
throw new TimeoutException("The opertion has timed out.");
}
}
}

public static async Task<TResult> WaitAsync<TResult>(this Task<TResult> task, TimeSpan timeout)
{
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var delayTask = Task.Delay(timeout, timeoutCancellationTokenSource.Token);
if (await Task.WhenAny(task, delayTask) == task)
{
timeoutCancellationTokenSource.Cancel();
return await task;
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}
}
}
}

这样我们就可以在任意的Task实例上调用myTask.WaitAsync来获取带超时的非阻塞异步等待了~~

参考资料 - [c# - Asynchronously wait for Task to complete with timeout - Stack Overflow](https://stackoverflow.com/q/4238345/6233938)