開源AwaitableCompletionSource,用於取代TaskCompletionSource

jiulang發表於2021-01-28

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

相關文章