在本文中,我們將學習如何使用TaskCompletionSource。它是您幾乎不需要使用的那些工具之一,但是當您這樣做時,您會很高興知道它。讓我們深入研究它。
基本用法
本節的源代碼位於Gigi Labs BitBucket存儲庫的TaskCompletionSource1文件夾中。
讓我們創建一個新的控制檯應用程序,在中Main()
,我們將具有在控制檯應用程序中運行異步代碼的常用解決方法:
1個
2
3
4
5
|
static void Main( string [] args) { Run(); Console.ReadLine(); } |
在該Run()
方法中,我們有一個簡單的示例顯示TaskCompletionSource的工作方式:
1個
2
3
4
5
6
7
8
9
|
static async void Run() { var tcs = new TaskCompletionSource< bool >(); var fireAndForgetTask = Task.Delay(5000) .ContinueWith(task => tcs.SetResult( true )); await tcs.Task; } |
TaskCompletionSource只是a的包裝Task
,可讓您控制其完成情況。因此,a TaskCompletionSource<bool>
將包含一個Task<bool>
,您可以bool
根據自己的邏輯設置結果。
在這裏,我們使用TaskCompletionSource作爲同步機制。我們的主線程使用TaskCompletionSource中的Task產生一個操作並等待其結果。即使該操作不是基於任務的,它也可以在TaskCompletionSource中設置Task的結果,從而允許主線程恢復其執行。
讓我們添加一些診斷代碼,以便我們可以瞭解輸出中發生的情況:
1個
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18歲
19
|
static async void Run() { var stopwatch = Stopwatch.StartNew(); var tcs = new TaskCompletionSource< bool >(); Console.WriteLine($ "Starting... (after {stopwatch.ElapsedMilliseconds}ms)" ); var fireAndForgetTask = Task.Delay(5000) .ContinueWith(task => tcs.SetResult( true )); Console.WriteLine($ "Waiting... (after {stopwatch.ElapsedMilliseconds}ms)" ); await tcs.Task; Console.WriteLine($ "Done. (after {stopwatch.ElapsedMilliseconds}ms)" ); stopwatch.Stop(); } |
這是輸出:
1個
2
3
|
Starting... (after 0ms) Waiting... (after 41ms) Done. (after 5072ms) |
如您所見,主線程一直等待直到tcs.SetResult(true)
被調用爲止。這觸發了TaskCompletionSource的基礎任務(主線程正在等待)的完成,並允許主線程恢復。
除了SetResult()
,TaskCompletionSource還提供了取消任務或將其錯誤處理的方法。也有安全Try...()
等效項:
SDK示例
本節的源代碼位於Gigi Labs BitBucket存儲庫中的TaskCompletionSource2文件夾中。
我發現TaskCompletionSource非常適合的一種情況是,當您獲得公開事件的第三方SDK時。想象一下:您通過SDK方法提交訂單,它爲該訂單提供了ID,但沒有結果。SDK將關閉並執行其可能要做的操作,以與外部服務進行對話並處理訂單。當這種情況最終發生時,SDK將觸發一個事件,以通知調用應用程序訂單是否成功下達。
我們將使用它作爲SDK代碼的示例:
1個
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class MockSdk { public event EventHandler<OrderOutcome> OnOrderCompleted; public Guid SubmitOrder( decimal price) { var orderId = Guid.NewGuid(); // do a REST call over the network or something Task.Delay(3000).ContinueWith(task => OnOrderCompleted( this , new OrderOutcome(orderId, true ))); return orderId; } } |
本OrderOutcome
類只是一個簡單的DTO:
1個
2
3
4
5
6
7
8
9
10
11
|
public class OrderOutcome { public Guid OrderId { get ; set ; } public bool Success { get ; set ; } public OrderOutcome(Guid orderId, bool success) { this .OrderId = orderId; this .Success = success; } } |
請注意MockSdk
,的SubmitOrder
不會返回任何形式的Task
,並且我們無法等待。這並不一定意味着它正在阻塞;它可能正在使用另一種形式的異步方式,例如異步編程模型或具有請求-響應方式的消息傳遞框架(例如RPC over RabbitMQ)。
歸根結底,這仍然是異步的,我們可以使用TaskCompletionSource在其之上構建基於任務的異步模式抽象(允許應用程序簡單地await
調用)。
首先,我們開始構建包裝SDK的簡單代理類:
1個
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class SdkProxy { private MockSdk sdk; public SdkProxy() { this .sdk = new MockSdk(); this .sdk.OnOrderCompleted += Sdk_OnOrderCompleted; } private void Sdk_OnOrderCompleted( object sender, OrderOutcome e) { // TODO } } |
然後,我們添加一個字典,該字典使我們能夠將每個OrderId與其對應的TaskCompletionSource關聯起來。使用ConcurrentDictionary而不是普通的Dictionary可幫助處理多線程方案而無需鎖定:
1個
2
3
4
5
6
7
8
9
10
11
12
|
private ConcurrentDictionary<Guid, TaskCompletionSource< bool >> pendingOrders; private MockSdk sdk; public SdkProxy() { this .pendingOrders = new ConcurrentDictionary<Guid, TaskCompletionSource< bool >>(); this .sdk = new MockSdk(); this .sdk.OnOrderCompleted += Sdk_OnOrderCompleted; } |
代理類公開了一個SubmitOrderAsync()
方法:
1個
2
3
4
5
6
7
8
9
10
11
|
public Task SubmitOrderAsync( decimal price) { var orderId = sdk.SubmitOrder(price); Console.WriteLine($ "OrderId {orderId} submitted with price {price}" ); var tcs = new TaskCompletionSource< bool >(); this .pendingOrders.TryAdd(orderId, tcs); return tcs.Task; } |
此方法調用SubmitOrder()
SDK中的基礎,並使用返回的OrderId在字典中添加新的TaskCompletionSource。Task
返回TaskCompletionSource的基礎,以便應用程序可以等待它。
1個
2
3
4
5
6
7
8
|
private void Sdk_OnOrderCompleted( object sender, OrderOutcome e) { string successStr = e.Success ? "was successful" : "failed" ; Console.WriteLine($ "OrderId {e.OrderId} {successStr}" ); this .pendingOrders.TryRemove(e.OrderId, out var tcs); tcs.SetResult(e.Success); } |
當SDK觸發完成事件時,代理將從待處理訂單中刪除TaskCompletionSource並設置其結果。等待基礎任務的應用程序將恢復並根據邏輯做出決定。
我們可以在控制檯應用程序中使用以下程序代碼對此進行測試:
1個
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
static void Main( string [] args) { Run(); Console.ReadLine(); } static async void Run() { var sdkProxy = new SdkProxy(); await sdkProxy.SubmitOrderAsync(10); await sdkProxy.SubmitOrderAsync(20); await sdkProxy.SubmitOrderAsync(5); await sdkProxy.SubmitOrderAsync(15); await sdkProxy.SubmitOrderAsync(4); } |
輸出顯示該程序確實確實在開始下一個訂單之前等待每個訂單完成:
1個
2
3
4
5
6
7
8
9
10
|
OrderId 3e2d4577-8bbb-46b7-a5df-2efec23bae6b submitted with price 10 OrderId 3e2d4577-8bbb-46b7-a5df-2efec23bae6b was successful OrderId e22425b9-3aa3-48db-a40f-8b8cfbdcd3af submitted with price 20 OrderId e22425b9-3aa3-48db-a40f-8b8cfbdcd3af was successful OrderId 3b5a2602-a5d2-4225-bbdb-10642a63f7bc submitted with price 5 OrderId 3b5a2602-a5d2-4225-bbdb-10642a63f7bc was successful OrderId ffd61cea-343e-4a9c-a76f-889598a45993 submitted with price 15 OrderId ffd61cea-343e-4a9c-a76f-889598a45993 was successful OrderId b443462c-f949-49b9-a6f0-08bbbb82fe7e submitted with price 4 OrderId b443462c-f949-49b9-a6f0-08bbbb82fe7e was successful |
摘要
使用TaskCompletionSource適應於異步任務使用的任意形式,並啓用優雅async
/ await
使用。
不要使用它只是爲異步方法公開異步包裝器。您要麼根本不這樣做,要麼改用Task.FromResult()。
如果您擔心異步調用可能永遠不會恢復,請考慮添加一個timeout。