开源AwaitableCompletionSource,用于取代TaskCompletionSource
1 TaskCompletionSource介绍
TaskCompletionSource提供创建未绑定到委托的任务,任务的状态由TaskCompletionSource上的方法显式控制,以支持未来的操作传播到它创建的任务。
使用场景
EAP(基于事件的异步模式)转TAP(基于任务的异步模式)
public static Task<string> DownloadStringAsync(Uri url)
{
var tcs = new TaskCompletionSource<string>();
var wc = new WebClient();
wc.DownloadStringCompleted += (s,e) =>
{
if (e.Error != null)
tcs.TrySetException(e.Error);
else if (e.Cancelled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(e.Result);
};
wc.DownloadStringAsync(url);
return tcs.Task;
}
结合CancellationTokenSource实现超时任务
public static async Task<string> DownloadStringAsync(Uri url, TimeSpan timeout)
{
var tcs = new TaskCompletionSource<string>();
var wc = new WebClient();
wc.DownloadStringCompleted += (s, e) =>
{
if (e.Error != null)
tcs.TrySetException(e.Error);
else if (e.Cancelled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(e.Result);
};
using var cts = new CancellationTokenSource();
cts.Token.Register(() => tcs.TrySetException(new TimeoutException()), useSynchronizationContext: false);
cts.CancelAfter(timeout);
wc.DownloadStringAsync(url);
return await tcs.Task;
}
不足之处
一个实例只支持创建一次任务
一个TaskCompletionSource<>实例,给它的任务设置结果或异常之后,这个实例就没有什么用了,既无法重置,也无法再创建新的任务实例。在高密集创建TaskCompletionSource<>要求的场景里,这可能给GC带来一点压力。
没有原生支持延时设置异常或结果功能
在网络请求里或更多场景里,可能会收不到或在特定的时间内收不到响应事件,这时TaskCompletionSource<>不得不和CancellationTokenSource结合使用,加上计时器完成超时功能,又多得创建一个对象实例。
2 AwaitableCompletionSource介绍
AwaitableCompletionSource的灵感来源于asp.netcore的kestrel的SocketAwaitableEventArgs,它把SocketAsyncEventArgs改装成支持单例可重复await的功能。AwaitableCompletionSource也支持单例可重复await,同时使用过后不再使用的实例还支持dispose回收到池中。
- 支持Singleton,单个实例持续使用;
- 支持Dispose后回收复用,创建实例0分配;
- 支持超时自动设置结果或异常,性能远好于TaskCompletionSource包装增加超时功能;
如何使用
使用方式与TaskCompletionSource大体一致。但是要使用静态类Create来创建实例,使用完成后Dispose实例。
var source = AwaitableCompletionSource.Create<string>();
ThreadPool.QueueUserWorkItem(s => ((IAwaitableCompletionSource)s).TrySetResult("1"), source);
Console.WriteLine(await source.Task);
// 支持多次设置获取结果
source.TrySetResultAfter("2", TimeSpan.FromSeconds(1d));
Console.WriteLine(await source.Task);
// 支持多次设置获取结果
source.TrySetResultAfter("3", TimeSpan.FromSeconds(2d));
Console.WriteLine(await source.Task);
// 实例使用完成之后,可以进行回收复用
source.Dispose();
3 性能比较
瞬态实例和调用TrySetResult
在频繁创建与回收AwaitableCompletionSource的场景,对于SetResult的使用,AwaitableCompletionSource的cpu时间明显高于TaskCompletionSource,但内存分配为0。
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
TaskCompletionSource_SetResult | 39.92 ns | 0.201 ns | 0.179 ns | 0.0229 | – | – | 96 B |
AwaitableCompletionSource_SetResult | 86.19 ns | 0.315 ns | 0.295 ns | – | – | – | – |
单例和调用TrySetResult
单例AwaitableCompletionSource的场景,对于SetResult的使用,AwaitableCompletionSource与TaskCompletionSource的cpu时间相当,内存分配为0。
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
TaskCompletionSource_SetResult | 41.46 ns | 0.744 ns | 1.180 ns | 0.0229 | – | – | 96 B |
AwaitableCompletionSource_SetResult | 49.30 ns | 0.528 ns | 0.494 ns | – | – | – | – |
瞬态实例和超时等待
注: TaskCompletionSource<>结合CancellationTokenSource<>实现超时。
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
TaskCompletionSource_WithTimeout | 237.0 ns | 4.76 ns | 5.85 ns | 0.1357 | – | – | 568 B |
AwaitableCompletionSource_WithTimeout | 176.6 ns | 0.83 ns | 0.74 ns | – | – | – | – |
单例超时等待
注:AwaitableCompletionSource单例,TaskCompletionSource瞬态。
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
TaskCompletionSource_WithTimeout | 233.1 ns | 4.59 ns | 6.58 ns | 0.1357 | – | – | 568 B |
AwaitableCompletionSource_WithTimeout | 131.5 ns | 1.41 ns | 1.32 ns | – | – | – | – |
4 总结
AwaitableCompletionSource在多个场景下可替代TaskCompletionSource,项目我已开源在https://github.com/xljiulang/AwaitableCompletionSource