非同步程式設計:.NET 4.5 基於任務的非同步程式設計模型(TAP)

風靈使發表於2018-06-12

最近我為大家陸續介紹了“IAsyncResult非同步程式設計模型 (APM)”和“基於事件的非同步程式設計模式(EAP)”兩種非同步程式設計模型。在.NET4.0Microsoft又為我們引入了新的非同步程式設計模型“基於任務的非同步程式設計模型(TAP)”,並且推薦我們在開發新的多執行緒應用程式中首選TAP,在.NET4.5中更是對TPL庫進行了大量的優化與改進。那現在我先介紹下TAP具有哪些優勢:

  1. 目前版本(.NET4.X)的任務排程器(TaskScheduler)依賴於底層的執行緒池引擎。通過區域性佇列的任務內聯化(task inlining)和工作竊取機制可以為我們提升程式效能。

  2. 輕鬆實現任務等待、任務取消、延續任務、異常處理(System.AggregateException)、GUI執行緒操作。

  3. 在任務啟動後,可以隨時以任務延續的形式註冊回撥。

  4. 充分利用現有的執行緒,避免建立不必要的額外執行緒。

  5. 結合C#5.0引入asyncawait關鍵字輕鬆實現“非同步方法”。

示例原始碼:非同步程式設計:.NET 4.5 基於任務的非同步程式設計模型(TAP).rar

術語:

APM 非同步程式設計模型,Asynchronous Programming Model

EAP 基於事件的非同步程式設計模式,Event-based Asynchronous Pattern

TAP 基於任務的非同步程式設計模式,Task-based Asynchronous Pattern

TPL 任務並行庫,Task Parallel Library

理解CLR執行緒池引擎、理解全域性佇列、理解執行緒的區域性佇列及效能優勢

1.CLR執行緒池引擎

CLR執行緒池引擎維護了一定數量的空閒工作執行緒以支援工作項的執行,並且能夠重用已有的執行緒以避免建立新的不必要的執行緒所花費的昂貴的處理過程。並且使用爬山演算法(hill-climbing algorithm)檢測吞吐量,判斷是否能夠通過更多的執行緒來完成更多的工作項。這個演算法的判斷依據是工作項所需某些型別資源的可用情況,例如:CPU、網路頻寬或其他。此外這個演算法還會考慮一個飽和點,即達到飽和點的時候,建立更多地執行緒反而會降低吞吐量。(執行緒池的詳細介紹請看《非同步程式設計:使用執行緒池管理執行緒》

目前版本的TAP的任務排程器(TaskScheduler)基於CLR執行緒池引擎實現。當任務排程器(TaskScheduler)開始分派任務時:

1)在主執行緒或其他並沒有分配給某個特定任務的執行緒的上下文中建立並啟動的任務,這些任務將會在全域性佇列中競爭工作執行緒。這些任務被稱為頂層任務。

2)然而,如果是在其他任務的上下文中建立的任務(子任務或巢狀任務),這些任務將被分配線上程的區域性佇列中。

  • 巢狀任務: 是在另一個任務的使用者委託中建立並啟動的任務。
  • 子任務:是使用TaskCreationOptions.AttachedToParent選項建立頂層任務的巢狀任務或延續任務;或使用TaskContinuationOptions.AttachedToParent選項建立的延續任務的巢狀任務或延續任務。(應用程式使用TaskCreationOptions.DenyChildAttach選項建立父任務。此選項指示執行時會取消子任務的AttachedToParent規範)

如果你不想特定的任務放入執行緒的區域性佇列,那麼可以指定TaskCreationOptions.PreferFairnessTaskContinuationOptions.PreferFairness列舉引數。(使TaskThreadPool.QueueUserWorkItem行為相同)

2.執行緒池的全域性佇列

當呼叫ThreadPool.QueueUserWorkItem()新增工作項時,該工作項會被新增到執行緒池的全域性佇列中。執行緒池中的空閒執行緒以FIFO的順序將工作項從全域性佇列中取出並執行,但並不能保證按某個指定的順序完成。

執行緒的全域性佇列是共享資源,所以內部會實現一個鎖機制。當一個任務內部會建立很多子任務時,並且這些子任務完成得非常快,就會造成頻繁的進入全域性佇列和移出全域性佇列,從而降低應用程式的效能。基於此原因,執行緒池引擎為每個執行緒引入了區域性佇列。

3.執行緒的區域性佇列為我們帶來兩個效能優勢:任務內聯化(task inlining)和工作竊取機制。

1)任務內聯化(task inlining)—-活用頂層任務工作執行緒

我們用一個示例來說明:

static void Main(string[] args)
{
    Task headTask= new Task(() =>
    {
        DoSomeWork(null);
    });
    headTask.Start();
    Console.Read();
}
private static void DoSomeWork(object obj)
{
    Console.WriteLine("任務headTask執行線上程“{0}”上",
        Thread.CurrentThread.ManagedThreadId);

    var taskTop = new Task(() =>
    {
        Thread.Sleep(500);
        Console.WriteLine("任務taskTop執行線上程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
    });
    var taskCenter = new Task(() =>
    {
        Thread.Sleep(500);
        Console.WriteLine("任務taskCenter執行線上程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
    });
    var taskBottom = new Task(() =>
    {
        Thread.Sleep(500);
        Console.WriteLine("任務taskBottom執行線上程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
    });
    taskTop.Start();
    taskCenter.Start();
    taskBottom.Start();
    Task.WaitAll(new Task[] { taskTop, taskCenter, taskBottom });
}

結果:

image

分析:(目前內聯機制只有出現在等待任務場景)

這個示例,我們從Main方法主執行緒中建立了一個headTask頂層任務並開啟。在headTask任務中又建立了三個巢狀任務並最後WaitAll() 這三個巢狀任務執行完成(巢狀任務安排在區域性佇列)。此時出現的情況就是headTask任務的執行緒被阻塞,而“任務內聯化”技術會使用阻塞的headTask的執行緒去執行區域性佇列中的任務。因為減少了對額外執行緒需求,從而提升了程式效能。

區域性佇列“通常”以LIFO的順序抽取任務並執行,而不是像全域性佇列那樣使用FIFO順序。LIFO順序通常用有利於資料區域性性,能夠在犧牲一些公平性的情況下提升效能。

資料區域性性的意思是:執行最後一個到達的任務所需的資料都還在任何一個級別的CPU快取記憶體中可用。由於資料在快取記憶體中任然是“熱的”,因此立即執行最後一個任務可能會獲得效能提升。

2)工作竊取機制—-活用空閒工作執行緒

當一個工作執行緒的區域性佇列中有很多工作項正在等待時,而存在一些執行緒卻保持空閒,這樣會導致CPU資源的浪費。此時任務排程器(TaskScheduler)會讓空閒的工作執行緒進入忙碌執行緒的區域性佇列中竊取一個等待的任務,並且執行這個任務。

由於區域性佇列為我們帶來了效能提升,所以,我們應儘可能地使用TPL提供的服務(任務排程器(TaskScheduler)),而不是直接使用ThreadPool的方法。

任務並行Task

一個任務表示一個非同步操作。任務執行的時候需要使用執行緒,但並不是說任務取代了執行緒,理解這點很重要。事實上,在《非同步程式設計:.NET4.X 資料並行》中介紹的System.Threading.Tasks.Parallel類構造的並行邏輯內部都會建立Task,而它們的並行和併發執行都是由底層執行緒支援的。任務和執行緒之間也沒有一對一的限制關係,通用語言執行時(CLR)會建立必要的執行緒來支援任務執行的需求。

1. Task簡單的例項成員

public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable
{
    public Task(Action<object> action, object state
          , CancellationToken cancellationToken,TaskCreationOptions creationOptions);

    // 獲取此 Task 例項的唯一 ID。
    public int Id { get; }
    // 獲取用於建立此任務的TaskCreationOptions。
    public TaskCreationOptions CreationOptions { get; }
    // 獲取此任務的TaskStatus。
    public TaskStatus Status { get; }
    // 獲取此 Task 例項是否由於被取消的原因而已完成執行。
    public bool IsCanceled { get; }
    // 獲取 Task 是否由於未經處理異常的原因而完成。
    public bool IsFaulted { get; }
    // 獲取導致 Task 提前結束的System.AggregateException。
    public AggregateException Exception { get; }

    #region IAsyncResult介面成員
    private bool IAsyncResult.CompletedSynchronously { get;}
    private WaitHandleIAsyncResult.AsyncWaitHandle { get; }

    // 獲取在建立 Task 時提供的狀態物件,如果未提供,則為 null。
    public object AsyncState { get; }
    // 獲取此 Task 是否已完成。
    public bool IsCompleted { get; }
    #endregion

    // 釋放由 Task 類的當前例項佔用的所有資源。
    public void Dispose();
    ……
}

分析:

1) CancellationTokenIsCancel

對於長時間執行的計算限制操作來說,支援取消是一件很“棒”的事情。.NET 4.0提供了一個標準的取消操作模式。即通過使用CancellationTokenSource建立一個或多個取消標記CancellationToken(cancellationToken可線上程池中執行緒或 Task 物件之間實現協作取消),然後將此取消標記傳遞給應接收取消通知的任意數量的執行緒或Task物件。當呼叫CancellationToken關聯的CancellationTokenSource物件的Cancle()時,每個取消標記(CancellationToken)上的IsCancellationRequested屬性將返回true。非同步操作中可以通過檢查此屬性做出任何適當響應。也可呼叫取消標記的ThrowIfCancellationRequested()方法來丟擲OperationCanceledException異常。

更多關於CancellationTokenCancellationTokenSource的介紹及示例請看《協作式取消》….

在Task任務中實現取消,可以使用以下幾種選項之一終止操作:

i.簡單地從委託中返回。在許多情況下,這樣已足夠;但是,採用這種方式“取消”的任務例項會轉換為RanToCompletion狀態,而不是Canceled 狀態。

ii.建立Task時傳入CancellationToken標識引數,並呼叫關聯CancellationTokenSource物件的Cancel()方法:

  • a)如果Task還未開始,那麼Task例項直接轉為Canceled狀態。(注意,因為已經Canceled狀態了,所以不能再在後面呼叫Start()
  • b)(見示例:TaskOperations.Test_Cancel();)如果Task已經開始,在Task內部必須丟擲OperationCanceledException異常(注意,只能存在OperationCanceledException異常,可優先考慮使用CancellationTokenThrowIfCancellationRequested()方法),Task例項轉為Canceled狀態。

    若對丟擲OperationCanceledException異常且狀態為CanceledTask進行等待操作(如:Wait/WaitAll),則會在Catch塊中捕獲到OperationCanceledException異常,但是此異常指示Task成功取消,而不是有錯誤的情況。因此IsCanceltrueIsFaultedfalseException屬性為null

iii.對於使用TaskContinuationOptions列舉值為NotOnOnlyOn建立的延續任務A,在其前面的任務結束狀態不匹配時,延續任務A將轉換為Canceled狀態,並且不會執行。

2)TaskCreationOptions列舉

定義任務建立、排程和執行的一些可選行為。

屬性 描述
None 指定應使用預設行為。
PreferFairness 較早安排的任務將更可能較早執行,而較晚安排執行的任務將更可能較晚執行。(Prefer:更喜歡 ; Fair:公平的)
LongRunning 該任務需要很長時間執行,因此,排程器可以對這個任務使用粗粒度的操作(預設TaskScheduler為任務建立一個專用執行緒,而不是排隊讓一個執行緒池執行緒來處理,可通過在延續任務中訪問:Thread.CurrentThread.IsThreadPoolThread屬性判別)。比如:如果任務可能需要好幾秒的時間執行,那麼就使用這個引數。相反,如果任務只需要不到1秒鐘的時間執行,那麼就不應該使用這個引數。
AttachedToParent 指定此列舉值的Task,其內部建立的Task或通過ContinueWith()建立的延續任務都為子任務。(父級是頂層任務)
DenyChildAttach 如果嘗試附加子任務到建立的任務,指定System.InvalidOperationException將被引發。
HideScheduler 建立任務的執行操作將被視為TaskScheduler.Default預設計劃程式。

3) IsCompleted

Task實現了IAsyncResult介面。在任務處於以下三個最終狀態之一時IsCompleted返回 true:RanToCompletionFaultedCanceled

4) TaskStatus列舉

表示 Task 的生命週期中的當前階段。一個Task例項只會完成其生命週期一次,即當Task到達它的三種可能的最終狀態之一時,Task就結束並釋放。

狀態 屬性 描述
可能的初始狀態 Created 該任務已初始化,但尚未被計劃。
WaitingForActivation 只有在其它依賴的任務完成之後才會得到排程的任務的初始狀態。這種任務是使用定義延續的方法建立的。
WaitingToRun 該任務已被計劃執行,但尚未開始執行。
中間狀態 Running 該任務正在執行,但尚未完成。
WaitingForChildrenToComplete 該任務已完成執行,正在隱式等待附加的子任務完成。
可能的最終狀態 RanToCompletion 已成功完成執行的任務。
Canceled 該任務已通過對其自身的CancellationToken引發OperationCanceledException異常
Faulted 由於未處理異常的原因而完成的任務。

狀態圖如下:

image

5)Dispose()

儘管Task為我們實現了IDisposable介面,但依然不推薦你主動呼叫Dispose()方法,而是由系統終結器進行清理。原因:

a) Task呼叫Dispose()主要釋放的資源是WaitHandle物件。

b) .NET4.5.NET4.0中提出的Task進行過大量的優化,讓其儘量不再依賴WaitHandle物件(eg:.NET4.0TaskWaitAll()/WaitAny()的實現依賴於WaitHandle)。

c) 在使用Task時,大多數情況下找不到一個好的釋放點,保證該Task已經完成並且沒有被其他地方在使用。

d)Task.Dispose()方法在“.NET Metro風格應用程式”框架所引用的程式集中甚至並不存在(即此框架中Task沒有實現IDisposable介面)。

更詳細更專業的Dispose()討論請看《.NET4.X並行任務Task需要釋放嗎?》

2.Task的例項方法

// 獲取用於等待此 Task 的等待者。
public TaskAwaiter GetAwaiter();
// 配置用於等待此System.Threading.Tasks.Task的awaiter。
// 引數:continueOnCapturedContext:
//     試圖在await返回時奪取原始上下文,則為 true;否則為 false。
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);

// 對提供的TaskScheduler同步執行 Task。
public void RunSynchronously(TaskScheduler scheduler);
// 啟動 Task,並將它安排到指定的TaskScheduler中執行。
public void Start(TaskScheduler scheduler);
// 等待 Task 完成執行過程。
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken);

// 建立一個在目標 Task 完成時執行的延續任務。
public Task ContinueWith(Action<Task, object> continuationAction, object state
        , CancellationToken cancellationToken
        , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task<TResult>ContinueWith<TResult>(
        Func<Task, object, TResult> continuationFunction
        , object state,CancellationToken cancellationToken
        , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
……

分析:

1)TaskContinuationOptions

在建立一個Task作為另一個Task的延續時,你可以指定一個TaskContinuationOptions引數,這個引數可以控制延續另一個任務的任務排程和執行的可選行為。
這裡寫圖片描述

注意:

a)如果使用預設選項TaskContinuationOptions.None,並且之前的任務被取消了,那麼延續任務任然會被排程並啟動執行。

b)如果該條件在前面的任務準備呼叫延續時未得到滿足,則延續將直接轉換為 Canceled 狀態,之後將無法啟動。

c)如果呼叫多工延續(即:呼叫TaskFactoryTaskFactory<TResult>的靜態ContinueWhenAllContinueWhenAny方法)時,NotOnOnlyOn六個標識或標識的組合都是無效的。也就是說,無論先驅任務是如何完成的,ContinueWhenAllContinueWhenAny都會執行延續任務。

d) TaskContinuationOptions.ExecuteSynchronously,指定同步執行延續任務。延續任務會使用前一個任務的資料,而保持在相同執行緒上執行就能快速訪問快取記憶體中的資料,從而提升效能。此外,也可避免排程這個延續任務產生不必要的額外執行緒開銷。
如果在建立延續任務時已經完成前面的任務,則延續任務將在建立此延續任務的執行緒上執行。只應同步執行執行時間非常短的延續任務。

2)開啟任務

只有Task處於TaskStatus.Created狀態時才能使用例項方法Start()。並且,只有在使用Task的公共建構函式構造的Task例項才能處於TaskStatus.Created狀態。

當然我們還知道有其他方式可以建立Task並開啟任務,比如Task.Run()/Task.ContinueWith()/Task.Factory.StartNew()/TaskCompletionSource/非同步方法(即使用asyncawait關鍵字的方法),但是這些方法返回的Task已經處於開啟狀態,即不能再呼叫Start()。更豐富更專業的討論請看《.NET4.X 並行任務中Task.Start()的FAQ》

3)延續任務ContinueWith

a) ContinueWith() 方法可建立一個根據TaskContinuationOptions引數限制的延續任務。可以為同一個Task定義多個延續任務讓它們並行執行。

比如,為t1定義兩個並行延續任務t2、t3.

Task<int> t1 = new Task<int>(() => { return 1; });
Task<int> t2 = t1.ContinueWith<int>(Work1,……);
Task<int> t3 = t1.ContinueWith<int>(Work1,……);

b)呼叫Wait()方法和Result屬性會導致執行緒阻塞,極有可能造成執行緒池建立一個新執行緒,這增大了資源的消耗,並損害了伸縮性。可以在延續任務中訪問這些成員,並做相應操作。

c)對前面任務的引用將以引數形式傳遞給延續任務的使用者委託,以將前面任務的資料傳遞到延續任務中。

4)Wait()

一個執行緒呼叫Wait()方法時,系統會檢查執行緒要等待的Task是否已開始執行。

a)如果是,呼叫Wait()的執行緒會阻塞,直到Task執行結束為止。

b)如果Task還沒有開始執行,系統可能(取決於區域性佇列的內聯機制)使用呼叫Wait()的執行緒來執行Task。如果發生這種情況,那麼呼叫Wait()的執行緒不會阻塞;它會執行Task並立刻返回。

  • i.這樣做的好處在於,沒有執行緒會被阻塞,所以減少了資源的使用(因為不需要建立一個執行緒來替代被阻塞的執行緒),並提升了效能(因為不需要花時間建立一個執行緒,也沒有上下文切換)。
  • ii.但不好的地方在於,假如執行緒在呼叫Wait()前已經獲得一個不可重入的執行緒同步鎖(eg:SpinLock),而Task試圖獲取同一個鎖,就會造成一個死鎖的執行緒!

5)RunSynchronously

可在指定的TaskSchedulerTaskScheduler.Current中同步執行Task。即RunSynchronously()之後的程式碼會阻塞到Task委託執行完畢。

示例如下:

Task task1 = new Task(() =>
{
    Thread.Sleep(5000);
    Console.WriteLine("task1執行完畢。");
});
task1.RunSynchronously();
Console.WriteLine("執行RunSynchronously()之後的程式碼。");

// 輸出==============================
// task1執行完畢。
// 執行RunSynchronously()之後的程式碼。

3.Task的靜態方法

// 返回當前正在執行的 Task 的唯一 ID。
public static int? CurrentId{ get; }
// 提供對用於建立 Task 和 Task<TResult>例項的工廠方法的訪問。
public static TaskFactory Factory { get; }
// 建立指定結果的、成功完成的Task<TResult>。
public static Task<TResult> FromResult<TResult>(TResult result);

// 建立將在指定延遲後完成的任務。
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken);

// 將線上程池上執行的指定工作排隊,並返回該工作的任務控制程式碼。
public static Task Run(Action action, CancellationToken cancellationToken);
// 將線上程池上執行的指定工作排隊,並返回該工作的 Task(TResult) 控制程式碼。
public static Task<TResult>  Run<TResult>(Func<TResult> function, CancellationToken  cancellationToken);
// 將線上程池上執行的指定工作排隊,並返回 function 返回的任務的代理項。
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
// 將線上程池上執行的指定工作排隊,並返回 function 返回的 Task(TResult) 的代理項。
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);

// 等待提供的所有 Task 物件完成執行過程。
public static bool WaitAll(Task[] tasks, intmillisecondsTimeout, CancellationToken cancellationToken);
// 等待提供的任何一個 Task 物件完成執行過程。
// 返回結果:
// 已完成的任務在 tasks 陣列引數中的索引,如果發生超時,則為 -1。
public static int WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);

// 所有提供的任務已完成時,建立將完成的任務。
public static Task WhenAll(IEnumerable<Task> tasks);
public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
// 任何一個提供的任務已完成時,建立將完成的任務。
public static Task<Task> WhenAny(IEnumerable<Task> tasks);
public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks);

// 建立awaitable,等待時,它非同步產生當前上下文。
// 返回結果:等待時,上下文將非同步轉換回等待時的當前上下文。
// 如果當前SynchronizationContext不為 null,則將其視為當前上下文。
// 否則,與當前執行任務關聯的任務計劃程式將視為當前上下文。
public static YieldAwaitable Yield();

分析:

1) FromResult<TResult>(TResult result);

建立指定結果的、成功完成的Task<TResult>。我們可以使用此方法建立包含預先計算結果/快取結果的 Task<TResult>物件,示例程式碼或CachedDownloads.cs示例檔案。

2) Delay

建立將在指定延遲後完成的任務,返回Task。可以通過awaitTask.Wait()來達到Thread.Sleep()的效果。儘管,Task.Delay()Thread.Sleep()消耗更多的資源,但是Task.Delay()可用於為方法返回Task型別;或者根據CancellationToken取消標記動態取消等待。

Task.Delay()等待完成返回的Task狀態為RanToCompletion;若被取消,返回的Task狀態為Canceled

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
Task.Factory.StartNew(() => { Thread.Sleep(1000); tokenSource.Cancel(); });
Console.WriteLine("Begin taskDelay1");
Task taskDelay1 = Task.Delay(100000, token);
try
{
    taskDelay1.Wait();
}
catch (AggregateException ae)
{
    foreach (var v in ae.InnerExceptions)
        Console.WriteLine(ae.Message + " " + v.Message);
}
taskDelay1.ContinueWith((t) =>Console.WriteLine(t.Status.ToString()));

Thread.Sleep(100);
Console.WriteLine();

Console.WriteLine("Begin taskDelay2");
Task taskDelay2 = Task.Delay(1000);
taskDelay2.ContinueWith((t) =>Console.WriteLine(t.Status.ToString()));
// 輸出======================================
// Begin taskDelay1
// 發生一個或多個錯誤。已取消一個任務。
// Canceled
// 
// Begin taskDelay2
// Completed

4. Task<TResult>:Task

Task<TResult>繼承自Task,表示一個可以返回值的非同步操作,提供Result只讀屬性用於訪問非同步操作的返回值。該屬性會阻塞執行緒,直到Task執行完畢並返回值。

System.Threading.Tasks.TaskFactory         

1.設定共用\預設的引數

通過TaskFactory物件提供的SchedulerCancellationTokenCreationOptionContinuationOptions屬性可以為Task設定共用\預設的引數,以便快捷的建立Task或延續任務。影響StartNew()ContinueWhenAll()|ContinueWhenAny()FromAsync()方法的預設引數設定。

2.StartNew()

Task.Factory.StartNew()可快速建立一個Task並且開啟任務。程式碼如下:

var t = Task.Factory.StartNew(someDelegate);

這等效於:

var t = new Task(someDelegate); 
t.Start();

表現方面,前者更高效。Start()採用同步方式執行以確保任務物件保持一致的狀態即使是同時呼叫多次Start(),也可能只有一個呼叫會成功。相比之下,StartNew()知道沒有其他程式碼能同時啟動任務,因為在StartNew()返回之前它不會將建立的Task引用給任何人,所以StartNew()不需要採用同步方式執行。更豐富更專業的討論請看《.NET4.X 並行任務中Task.Start()的FAQ》

3. ContinueWhenAll()

public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction
    , CancellationToken cancellationToken
    , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);

建立一個延續 Task 或延續 Task<TResult>,它將在提供的一組任務完成後馬上開始。延續任務操作委託接受一個Task[]陣列做引數。

4. ContinueWhenAny()

public Task ContinueWhenAny(Task[] tasks, Action<Task> continuationAction
    , CancellationToken cancellationToken
    , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);

建立一個延續 Task 或延續 Task<TResult>,它將在提供的組中的任何一個任務完成後馬上開始。延續任務操作委託接受一個 Task 做引數。

5.通過Task.TaskFactory.FromAsync() 例項方法,我們可以將APM轉化為TAP。示例見此文的後面小節“AMP轉化為TAP和EAP轉化為TAP”。

System.Threading.Tasks.TaskScheduler        

TaskScheduler表示一個處理將任務排隊到執行緒中的底層工作物件。TaskScheduler通常有哪些應用呢?

  1. TaskScheduler是抽象類,可以繼承它實現自己的任務排程計劃。如:預設排程程式ThreadPoolTaskScheduler、與SynchronizationContext.Current關聯的SynchronizationContextTaskScheduler

  2. TaskScheduler.Default獲取預設排程程式ThreadPoolTaskScheduler

  3. TaskScheduler.Current獲取當前任務執行的TaskScheduler

  4. TaskScheduler.TaskSchedulerFromCurrentSynchronizationContext() 方法獲取與SynchronizationContext.Current關聯的SynchronizationContextTaskSchedulerSynchronizationContextTaskScheduler上的任務都會通過SynchronizationContext.Post()在同步上下文中進行排程。通常用於實現跨執行緒更新控制元件。

  5. 通過MaximumConcurrencyLevel設定任務排程計劃能支援的最大併發級別。

  6. 通過UnobservedTaskException事件捕獲未被觀察到的異常。

System.Threading.Tasks.TaskExtensions

提供一組用於處理特定型別的 Task 例項的靜態方法。將特定Task例項進行解包操作。

public static class TaskExtensions
{
    public static Task<TResult> Unwrap<TResult>(this Task<Task<TResult>> task);
    public static Task Unwrap(this Task<Task> task);
}

AMP轉化為TAP和EAP轉化為TAP

1.AMP轉化為TAP

通過Task.TaskFactory.FromAsync() 例項方法,我們可以將APM轉化為TAP

注意點:

1) FromAsync方法返回的任務具有WaitingForActivation狀態,並將在建立該任務後的某一時間由系統啟動。如果嘗試在這樣的任務上呼叫 Start,將引發異常。

2)轉化的APM非同步模型必須符合兩個模式:

a)接受Begin***End***方法。此時要求Begin***方法簽名的委託必須是AsyncCallback以及 End***方法只接受IAsyncResult一個引數。此模式AsyncCallback回撥由系統自動生成,主要工作是呼叫End***方法。

public Task<TResult> FromAsync<TArg1, TResult>(
      Func<TArg1, AsyncCallback, object, IAsyncResult> beginMethod
    , Func<IAsyncResult, TResult> endMethod, TArg1 arg1
    , object state, TaskCreationOptions creationOptions);

b)接受IAsyncResult物件以及End***方法。此時Begin***方法的簽名已經無關緊要只要(即:此模式支援傳入自定義回撥委託)能返回IAsyncResult的引數以及 End***方法只接受IAsyncResult一個引數。

public Task<TResult> FromAsync<TResult>(IAsyncResult asyncResult
    , Func<IAsyncResult, TResult> endMethod);

3)當然,我們有時需要給客戶提供統一的 Begin***()End***() 呼叫方式,我們可以直接使用Task從零開始構造APM。即:在 Begin***() 建立並開啟任務,並返回Task。因為Task是繼承自IAsyncResult介面的,所以我們可以將其傳遞給 End***() 方法,並在此方法裡面呼叫Result屬性來等待任務完成。

4) 對於返回的Task,可以隨時以任務延續的形式註冊回撥。

現在將在《APM非同步程式設計模型》博文中展現的示例轉化為TAP模式。關鍵程式碼如下:

public Task<int> CalculateAsync<TArg1, TArg2>(
      Func<TArg1, TArg2, AsyncCallback, object, IAsyncResult> beginMethod
    , AsyncCallback userCallback, TArg1 num1, TArg2 num2, object asyncState)
{
    IAsyncResult result = beginMethod(num1, num2, userCallback, asyncState);
    return Task.Factory.FromAsync<int>(result
            , EndCalculate, TaskCreationOptions.None);
}

public Task<int> CalculateAsync(int num1, int num2, object asyncState)
{
    return Task.Factory.FromAsync<int, int, int>(BeginCalculate, EndCalculate
            , num1, num2, asyncState, TaskCreationOptions.None);
}

2.EAP轉化為TAP

我們可以使用TaskCompletionSource<TResult>例項將EAP操作表示為一個Task<TResult>

TaskCompletionSource<TResult>表示未繫結委託的Task<TResult>的製造者方,並通過TaskCompletionSource<TResult>.Task屬性獲取由此Tasks.TaskCompletionSource<TResult>建立的Task<TResult>

注意,TaskCompletionSource<TResult>建立的任何任務將由TaskCompletionSource啟動,因此,使用者程式碼不應在該任務上呼叫 Start()方法。

public class TaskCompletionSource<TResult>
{
    public TaskCompletionSource();
    // 使用指定的狀態和選項建立一個TaskCompletionSource<TResult>。
    //   state: 要用作基礎 Task<TResult>的AsyncState的狀態。
    public TaskCompletionSource(object state, TaskCreationOptions creationOptions);

    // 獲取由此Tasks.TaskCompletionSource<TResult>建立的Tasks.Task<TResult>。
    public Task<TResult> Task { get; }

    // 將基礎Tasks.Task<TResult>轉換為Tasks.TaskStatus.Canceled狀態。
    public void SetCanceled();
    public bool TrySetCanceled();

    // 將基礎Tasks.Task<TResult>轉換為Tasks.TaskStatus.Faulted狀態。
    public void SetException(Exception exception);
    public void SetException(IEnumerable<Exception> exceptions);
    public bool TrySetException(Exception exception);
    public bool TrySetException(IEnumerable<Exception> exceptions);

    // 嘗試將基礎Tasks.Task<TResult>轉換為TaskStatus.RanToCompletion狀態。
    public bool TrySetResult(TResult result);
    ……        
}

現在我將在《基於事件的非同步程式設計模式(EAP)》博文中展現的BackgroundWorker2元件示例轉化為TAP模式。

我們需要修改地方有:

1) 建立一個TaskCompletionSource<int>例項tcs;

2) 為tcs.Task返回的任務建立延續任務,延續任務中根據前面任務的IsCanceledIsFaultedResult等成員做邏輯;

3) Completed事件,在這裡面我們將設定返回任務的狀態。

關鍵程式碼如下:

    // 1、建立 TaskCompletionSource<TResult>
tcs = new TaskCompletionSource<int>();
worker2.RunWorkerCompleted += RunWorkerCompleted;
    // 2、註冊延續
tcs.Task.ContinueWith(t =>
{
        if (t.IsCanceled)
            MessageBox.Show("操作已被取消");
        else if (t.IsFaulted)
            MessageBox.Show(t.Exception.GetBaseException().Message);
        else
            MessageBox.Show(String.Format("操作已完成,結果為:{0}", t.Result));
    }, TaskContinuationOptions.ExecuteSynchronously);
    // 3、執行非同步任務
    worker2.RunWorkerAsync();
    // 4、Completed事件
    private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Error != null)
            tcs.SetException(e.Error);
        else if (e.Cancelled)
            tcs.SetCanceled();
        else
            tcs.SetResult((int)e.Result);
        // 登出事件,避免多次掛接事件
        worker2.RunWorkerCompleted -= RunWorkerCompleted;
    }

使用關鍵字async和await實現非同步方法

C#5.0中引入了asyncawait關鍵字,可以方便我們使用順序結構流(即不用回撥)來實現非同步程式設計,大大降低了非同步程式設計的複雜程度。(vs2010打 Visual Studio Async CTP for VS2010補丁可以引入關鍵字”async”和”await”的支援,但是得不到.net4.5新增API的支援)

非同步方法的實現原理

  • 非同步方法不需要多執行緒,因為一個非同步方法並不是執行在一個獨立的執行緒中的。
  • 非同步方法執行在當前同步上下文中,只有啟用的時候才佔用當前執行緒的時間。
  • 非同步模型採用時間片輪轉來實現。

非同步方法的引數和返回值

  • 非同步方法的引數:

    不能使用“ref”引數和“out”引數,但是在非同步方法內部可以呼叫含有這些引數的方法

  • 非同步方法的返回型別:

    Task<TResult>:Tresult為非同步方法的返回值型別。

    Task:非同步方法沒有返回值。

    void:主要用於事件處理程式(不能被等待,無法捕獲異常)。非同步事件通常被認為是一系列非同步操作的開始。使用void返回型別不需要await,而且呼叫void非同步方法的函式不會捕獲方法丟擲的異常。(非同步事件中使用await,倘若等待的任務由有異常會導致丟擲“呼叫的目標發生了異常”。當然你可以在非同步事件中呼叫另一個有返回值的非同步方法)

非同步方法的命名規範

  • 非同步方法的方法名應該以Async作為字尾
  • 事件處理程式,基類方法和介面方法,可以忽略此命名規範:例如:
    startButton_Click不應重新命名為startButton_ClickAsync

asyncawait關鍵字不會導致其他執行緒的建立,執行非同步方法的執行緒為其呼叫執行緒。而非同步方法旨在成為非阻塞操作,即當await等待任務執行時,非同步方法會將控制權轉移給非同步方法外部,讓其不受阻塞的繼續執行,待await等待的任務執行完畢再將控制權轉移給await處,繼續執行非同步方法後續的程式碼。

1.我們可通過下圖來明白非同步方法的構建和非同步方法的執行流程。(程式碼詳見我提供的示例程式async_await_method專案)

這裡寫圖片描述

需要注意的一個問題:被“async”關鍵字標記的方法的呼叫都會強制轉變為非同步方式嗎?

不會,當你呼叫一個標記了”async”關鍵字的方法,它會在當前執行緒以同步的方式開始執行。所以,如果你有一個同步方法,它返回void並且你做的所有改變只是將其標記的“async”,這個方法呼叫依然是同步的。返回值為TaskTask<TResult>也一樣。

方法用“async”關鍵字標記不會影響方法是同步還是非同步執行並完成,而是,它使方法可被分割成多個片段,其中一些片段可能非同步執行,這樣這個方法可能非同步完成。這些片段界限就出現在方法內部顯示使用“await”關鍵字的位置處。所以,如果在標記了“async”的方法中沒有顯示使用“await”,那麼該方法只有一個片段,並且將以同步方式執行並完成。

2.編譯器轉換

使用 async 關鍵字標記方法,會導致 C#Visual Basic 編譯器使用狀態機重新編寫該方法的實現。藉助此狀態機,編譯器可以在該方法中插入多箇中斷點,以便該方法可以在不阻止執行緒的情況下,掛起和恢復其執行。這些中斷點不會隨意地插入。它們只會在您明確使用 await 關鍵字的位置插入:

private async void btnDoWork_Click(object sender, EventArgs e)
{
    ...
    await someObject; // <-- potential method suspension point
    ...
}

當您等待未完成的非同步操作時,編譯器生成的程式碼可確保與該方法相關的所有狀態(例如,區域性變數)封裝並保留在堆中。然後,該函式將返回到呼叫程式,允許在其執行的執行緒中執行其他任務。當所等待的非同步操作在稍後完成時,該方法將使用保留的狀態恢復執行。

任何公開 await 模式的型別都可以進行等待。該模式主要由一個公開的 GetAwaiter()方法組成,該方法會返回一個提供 IsCompletedOnCompletedGetResult 成員的型別。當您編寫以下程式碼時:

await someObject;

編譯器會生成一個包含 MoveNext 方法的狀態機類:

private class FooAsyncStateMachine : IAsyncStateMachine
{ 
    // Member fields for preserving “locals” and other necessary     state 
    int $state; 
    TaskAwaiter $awaiter; 
    … 
    public void MoveNext() 
    { 
        // Jump table to get back to the right statement upon         resumption 
        switch (this.$state) 
        { 
            … 
        case 2: goto Label2; 
            … 
        } 
        … 
        // Expansion of “await someObject;” 
        this.$awaiter = someObject.GetAwaiter(); 
        if (!this.$awaiter.IsCompleted) 
        { 
            this.$state = 2; 
            this.$awaiter.OnCompleted(MoveNext); 
            return; 
            Label2: 
        } 
        this.$awaiter.GetResult(); 
        … 
    } 
}

在例項someObject上使用這些成員來檢查該物件是否已完成(通過 IsCompleted),如果未完成,則掛接一個續體(通過 OnCompleted),當所等待例項最終完成時,系統將再次呼叫 MoveNext 方法,完成後,來自該操作的任何異常將得到傳播或作為結果返回(通過 GetResult),並跳轉至上次執行中斷的位置。

3.自定義型別支援等待

如果希望某種自定義型別支援等待,我們可以選擇兩種主要的方法。

1)一種方法是針對自定義的可等待型別手動實現完整的 await 模式,提供一個返回自定義等待程式型別的 GetAwaiter 方法,該等待程式型別知道如何處理續體和異常傳播等等。

2)第二種實施該功能的方法是將自定義型別轉換為Task,然後只需依靠對等待任務的內建支援來等待特殊型別。前文所展示的“EAP轉化為TAP”正屬於這一類,關鍵程式碼如下:


private async void btn_Start_Click(object sender, EventArgs e)
{
    this.progressBar1.Value = 0;

    tcs = new TaskCompletionSource&lt;int&gt;();
    worker2.RunWorkerCompleted += RunWorkerCompleted;
    tcs.Task.ContinueWith(t =&gt;
    {
        if (t.IsCanceled)
            MessageBox.Show("操作已被取消");
        else if (t.IsFaulted)
            MessageBox.Show(t.Exception.GetBaseException().Message);
        else
            MessageBox.Show(String.Format("操作已完成,結果為:{0}", t.Result));
    }, TaskContinuationOptions.ExecuteSynchronously);

    worker2.RunWorkerAsync();
    // void的非同步方法:主要用於事件處理程式(不能被等待,無法捕獲異常)。非同步事件通常被認為
    // 是一系列非同步操作的開始。使用void返回型別不需要await,而且呼叫void非同步方法的函式不
    // 會捕獲方法丟擲的異常。(非同步事件中使用await,倘若等待的任務由有異常會導致
    // 丟擲“呼叫的目標發生了異常”。當然你可以在非同步事件中呼叫另一個有返回值的非同步方法)

    // 所以不需要下面的await,因為會出現在執行取消後拖動介面會因異常被觀察到並且終止整個程式
    // await tcs.Task;
}

處理TAP中的異常

在任務丟擲的未處理異常都封裝在System.AggregateException物件中。這個物件會儲存在方法返回的TaskTask<TResult>物件中,需要通過訪問Wait()ResultException成員才能觀察到異常。(所以,在訪問Result之前,應先觀察IsCanceledIsFaulted屬性)

1.AggregateException物件的三個重要成員

1)InnerExceptions屬性

獲取導致當前異常的System.Exception例項的只讀集合(即,ReadOnlyCollection)。不要將其與基類Exception提供的InnerException屬性混淆。

2)Flatten() 方法

遍歷InnerExceptions異常列表,若列表中包含型別為AggregateException的異常,就移除所有巢狀的AggregateException,直接返回其真真的異常資訊(效果如下圖)。
這裡寫圖片描述

1)Handle(Func<Exception, bool> predicate)方法

它為AggregateException中包含的每個異常都呼叫一個回撥方法。然後,回撥方法可以為每個異常決定如何對其進行處理,回撥返回true表示異常已經處理,返回false表示沒有。在呼叫Handle之後,如果至少有一個異常沒有處理,就建立一個新的AggregateException物件,其中只包含未處理的異常,並丟擲這個新的AggregateException物件。

比如:將任何OperationCanceledException物件都視為已處理。其他任何異常都造成丟擲一個新的AggregateException,其中只包含未處理的異常。

try{……}
catch (AggregateException ae)
{
    ae.Handle(e => e is OperationCanceledException);
}

1.父任務生成了多個子任務,而多個子任務都丟擲了異常

1) 巢狀子任務

Task t4 = Task.Factory.StartNew(() =>
{
    Task.Factory.StartNew(() => { throw new Exception("子任務Exception_1"); }
            , TaskCreationOptions.AttachedToParent);

    Task.Factory.StartNew(() => { throw new Exception("子任務Exception_2"); }
            , TaskCreationOptions.AttachedToParent);

    throw new Exception("父任務Exception");
});

對於“巢狀子任務”中子任務的異常都會包裝在父任務返回的TaskTask<TResult>物件中。如此例子中 t4.Exception.InnerExceptionsCount為3。

對於子任務返回的異常型別為包裝過的AggregateException物件,為了避免迴圈訪問子任務異常物件的InnerExceptions才能獲取真真的異常資訊,可以使用上面提到的Flatten() 方法移除所有巢狀的AggregateExceprion

2)Continue子任務

Task t1 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(500);   // 確保已註冊好延續任務
    throw new Exception("父任務Exception");
}, TaskCreationOptions.AttachedToParent);
Task t2 = t1.ContinueWith((t) =>
{
    throw new Exception("子任務Exception_1");
});
Task t3 = t1.ContinueWith((t) =>
{
    throw new Exception("子任務Exception_2");
});

對於“Continue子任務”中的子任務其異常與父任務是分離的,各自包裝在自己返回的TaskTask<TResult>物件中。如此示例 t1、t2、t3 的Exception.InnerExceptionsCount都為1。

2.TaskSchedulerUnobservedTaskException事件

假如你一直不訪問TaskWait()ResultException成員,那麼你將永遠注意不到這些異常的發生。為了幫助你檢測到這些未處理的異常,可以向TaskScheduler物件的UnobservedTaskException事件註冊回撥函式。每當一個Task被垃圾回收時,如果存在一個沒有注意到的異常,CLR的終結器執行緒會引發這個事件。

可在事件回撥函式中呼叫UnobservedTaskExceptionEventArgs物件的SetObserved() 方法來指出已經處理好了異常,從而阻止CLR終止執行緒。然而並不推薦這麼做,寧願終止程式也不要帶著已經損壞的狀態繼續執行。

示例程式碼:(要監控此程式碼必須在GC.Collect();和事件裡兩個地方進行斷點)

TaskScheduler.UnobservedTaskException += (s, e) =>
{
    //設定所有未覺察異常已被處理
    e.SetObserved();
};
Task.Factory.StartNew(() =>
{
    throw new Exception();
});
//確保任務完成
Thread.Sleep(100);
//強制垃圾會受到,在GC回收時才會觸發UnobservedTaskException事件
GC.Collect();
//等待終結器處理
GC.WaitForPendingFinalizers();

3.返回voidasync“非同步方法”中的異常

我們已經知道返回TaskTask<TResult>物件的任務中丟擲的異常會隨著返回物件一起返回,可通過Exception屬性獲取。這對於返回TaskTask<TResult>物件的“非同步方法”情況也是一樣。

然而對於返回void的“非同步方法”,方法中丟擲的異常會直接導致程式奔潰。

public static async void Test_void_async_Exception()
{
    throw new Exception();
}

另外,我們還要特別注意lambda表示式構成的“非同步方法”,如:

Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); });

本博文主要介紹了Task的重要API、任務的CLR執行緒池引擎、TaskFactory物件、TaskScheduler物件、TaskExtensions物件、AMP轉化為TAPEAP轉化為TAP、使用關鍵字asyncawait實現非同步方法以及自定義型別支援等待、處理TAP中的異常。

===========================================================

本篇博文基於.NET4.5中TPL所寫。對於.NET4.0中TPL會有些差異,若有園友知道差異還請告知,我這邊做個記錄方便大家也方便自己。

1、.NET4.0中TPL未觀察到的異常會在GC回收時終止程式。

相關文章