通過示例TASKCOMPLETIONSOURCE

在本文中,我們將學習如何使用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適應於異步任務使用的任意形式,並啓用優雅asyncawait使用。

不要使用它只是爲異步方法公開異步包裝器。您要麼根本不這樣做,要麼改用Task.FromResult()

如果您擔心異步調用可能永遠不會恢復,請考慮添加一個timeout

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章