基於Task的非同步模式的定義
命名,參數和傳回型別
在TAP(Task-based Asynchronous Pattern)中的非同步作業的啟動和完成是通過一個單獨的方法來表現的,因此只有一個方法要命名。這與IAsyncResult模式或者APM(Asynchronous Programming Model,非同步編程模型)模式形成對比,後者必須要有開始方法名和結束方法名;還與基於事件(event-based)的非同步模式(EAP)不同,它們要求方法名以Async為尾碼,而且要求一個或多個事件,事件控制代碼委託類型和派生自Event參數的類型。TAP中的非同步方法呼叫使用“Async”尾碼命名,跟在操作名稱的後面(例如MethodNameAsync)。TAP中的非同步方法呼叫返回一個Task類型或者Task<TResult>,基於相應的同步方法是否分別返回一個void或者TResult類型。
比如,思考下面的“Read”方法,它將特定數量的資料讀取到一個以特定位移量的buffer中:
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}
這個方法對應的APM版本則有下面兩個方法:
public class MyClass
{
public IAsyncResult BeginRead(byte[] buffer, int offset, int count,AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}
EAP版本對應的方法是這樣的:
public class MyClass
{
public void ReadAsync(byte[] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}
public delegate void ReadCompletedEventHandler(object sender, ReadCompletedEventArgs eventArgs);
public class ReadCompletedEventArgs: AsyncCompletedEventArgs
{
public int Result {
get;
}
}
TAP對應的版本只有下面一個方法:
public class MyClass
{
public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}
一個基本的TAP方法的參數應該和同步方法的參數相同,且順序相同。然而,“out”和“ref”參數不遵從這個規則,並且應該避免使用它們。通過out或者ref返回的任何資料可以作為返回的Task<TResult>結果的一部分,可以利用一個元組或者一個自訂資料結構容納多個值。
純粹致力於建立,操作,或組合的任務方法(該方法的非同步目的在方法名上或者在方法上以類型命名是明確的)不需要遵循上述命名模式;這些方法通常被稱為"組合子"。這種方法的例子包括Task. WhenAll和Task.WhenAny,本文檔後面的會更深入地討論。
表現
初始化非同步作業
在返回結果的任務之前,基於TAP非同步方法呼叫允許同步地處理少量的工作。這項工作應保持在所需的最低數量,執行如驗證參數和啟動非同步作業的操作。很可能從使用介面執行緒將調用非同步方法呼叫,因此所有長時間啟動並執行非同步方法呼叫的同步前期部分工作可能會損害響應能力。很有可能同時將啟動多個非同步方法呼叫,因此所有長時間啟動並執行非同步方法呼叫的同步前期部分工作可能會延遲啟動其他非同步作業,從而減少並發的好處。
在某些情況下,完成操作所需的工作量小於非同步啟動操作需要的工作量(例如,從流中讀取資料,這個讀取操作可以被已經緩衝在記憶體中的資料所滿足)。在這種情況下,操作可能同步完成,返回一個已經完成的任務。
異常
一個非同步方法呼叫只應該直接捕獲一個MethodNameAsync 調用時拋出的異常以響應用法錯誤。對於其他所有的錯誤,在非同步方法呼叫執行期間發生的異常應該分配給返回的任務。這種情況是在Task返回之前,非同步方法呼叫同步完成下發生的。一般地,一個Task至多包含一個異常。然而,對於一個Task表示多個操作(如,Task.WhenAll)的情況,單個Task也會關聯多個異常。
【*每個.Net設計指南都指出,一個用法錯誤可以通過改變調用方法的碼來避免。比如,當把null作為一個方法的參數傳遞時,錯誤狀態就會發生,錯誤條件通常被表示為ArgumentNullException,開發人員可以修改調用碼來確保null沒有傳遞過。換言之,開發人員可以並且應該確保用法錯誤從來沒有在生產代碼中發生過】。
目標環境
非同步執行的發生取決於TAP方法的實現。TAP方法的開發人員可能選擇線上程池上執行工作負載,也可能選擇使用非同步 I/O實現它,因而沒有被綁定到大量操作執行的線程上,也可以選擇在特定的線程上運行,如UI線程,或者其他一些潛在的上下文。甚至可能是這種情況,TAP方法沒有東西執行,簡單返回一個在系統中其他地方情況發生的Task(如Task<TData>表示TData到達一個排隊的資料結構)。
TAP方法的調用者也可能阻塞等待TAP方法的完成(通過在結果的Task上同步地等待),或者利用延續在非同步作業完成時執行附加代碼。延續建立者在延續代碼執行的地方有控制權。這些延續代碼要麼通過Task類(如ContinueWith)顯示地建立,要麼使用語言支援隱式地建立在延續代碼之上(如C#中的“await”)。
Task狀態
Task類提供了非同步作業的生命週期,該生命週期通過TaskStatus枚舉表示。為了支援派生自Task和Task<TResult>類型的案例,以及來自調度的構建分離,Task類暴露了一個Start方法。通過public建構函式建立的Tasks被稱為“冷”任務,在“冷”任務中,它們以非調度(non-scheduled)的TaskStatus.Created狀態開始生命週期。直到在這些執行個體上Start調用時,它們才促使被調度。所有在“熱”狀態開始生命週期的其他task,意味著它們表示的非同步作業已經初始化了,它們的TaskStatus是一個除Created之外的其它枚舉值。
所有從TAP方法返回的tasks肯定是“熱的”。如果TAP方法內部使用一個Task的建構函式來執行個體化要返回的task,那麼此TAP方法必須在返回task之前在Task對象上調用Start方法。TAP方法的消費者可以安全地假定返回的task是“熱的”,並不應該嘗試在任何返回自TAP方法的Task上調用Start。在“熱的”task上調用Start會導致InvalidOperationException (Task類自動處理這個檢查)。
可選:撤銷
TAP中的撤銷對於非同步方法呼叫的實現者和非同步方法呼叫的消費者都是選擇加入的。如果一個操作將要取消,那麼它會暴露
一個接受System.Threading.CancellationToken的MethodNameAsync 的重載。非同步作業會監視對於撤銷請求的這個token,如果接收到了撤銷請求,可以選擇處理該請求並取消操作。如果處理請求導致任務過早地結束,那麼從TAP方法返回的Task會以TaskStatus.Canceled狀態結束。
為了暴露一個可取消的非同步作業,TAP實現提供了在同步對應的方法的參數後接受一個CancellationToken的重載。按照慣例,該參數命名為“cancellationToken”。
public Task<int> ReadAsync(
byte [] buffer, int offset, int count,
CancellationToken cancellationToken);
如果token已經請求了撤銷並且非同步作業尊重該請求,那麼返回的task將會以TaskStatus.Canceled狀態結束,將會產生沒有可利用的Result,並且沒有異常。Canceled狀態被認為是一個伴隨著Faulted和RanToCompletion 狀態的任務最終或完成的狀態。因此,Canceled 狀態的task的IsCompleted 屬性返回true。當一個Canceled 狀態的task完成時,任何用該task註冊的延續操作都會被調度或執行,除非這些延續操作通過具體的TaskContinuationOptions 用法在被建立時取消了(如TaskContinuationOptions.NotOnCanceled)。任何非同步地等待一個通過語言特性使用的撤銷的task的代碼將會繼續執行並且收到一個OperationCanceledException(或派生於該異常的類型)。在該task(通過Wait 或WaitAll方法)上同步等待而阻塞的任何代碼也會繼續執行並拋出異常。
如果CancellationToken已經在接受那個token的TAP方法調用之前發出了取消請求,那麼該TAP方法必須返回一個Canceled狀態的task。然而,如果撤銷在非同步作業執行期間請求,那麼非同步作業不需要尊重該撤銷請求。只有由於撤銷請求的操作完成時,返回的Task才會以Canceled 狀態結束。如果一個撤銷被請求了,但是結果或異常仍產生了,那麼Task將會分別以RanToCompletion或 Faulted 的狀態結束。
首先,在使用非同步方法呼叫的開發人員心目中,那些渴望撤銷的方法,需要提供一個接受CancellationToken變數的重載。對於不可取消的方法,不應該提供接受CancellationToken的重載。這個有助於告訴調用者目標方法實際上是否是可取消的。不渴望撤銷的消費者可以調用一個接受CancellationToken的方法來把CancellationToken.None作為提供的參數值。CancellationToken.None功能上等價於default(CancellationToken)。
可選:進度報告
一些非同步作業得益於提供的進度通知,一般利用這些進度通知來更新關於非同步作業進度的UI。
在TAP中,進度通過IProgress<T>介面傳遞給非同步方法呼叫的名為“progress”的參數來處理。在該非同步方法呼叫調用時提供這個進度介面有助於消除來自於錯誤的用法的競爭條件,這些錯誤的用法 是因為在此操作可能錯過更新之後,事件控制代碼錯誤地註冊導致的。更重要的是,它使變化的進度實現可被利用,因為由消費者決定。比如,消費者肯僅僅關心最新的進度更新,或者可能緩衝所有更新,或者可能僅僅想要為每個更新調用一個action,或者可能想控制是否調用封送到特定的線程。所有這些可能通過使用一個不同的介面的實現來完成,每一個介面可以定製到特殊的消費者需求。因為有了撤銷,如果API支援進度通知,那麼TAP實現應該只提供一個IProgress<T>參數。
比如,如果我們上面提到的ReadAsync方法可以以迄今讀取位元組數的形式能報告中間的進度,那麼進度的回調(callback)可以是一個IProgress<int>:
public Task<int> ReadAsync(
byte [] buffer, int offset, int count,
IProgress<int> progress);
如果FindFilesAsync方法返回一個所有檔案的列表,該列表滿足一個特殊的搜尋模式,那麼進度回調可以提供完成工作的百分比和當前部分結果集的估計。它也可以這樣處理元組,如:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<Tuple<double,ReadOnlyCollection<List<FileInfo>>>> progress);
或者使用API具體的資料類型,如:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<FindFilesProgressInfo> progress);
在後一種情況,特殊的資料類型以“ProgressInfo”為尾碼。
如果TAP實現提供了接受progress參數的重載,那麼它們必須允許參數為null,為null的情況下,進度不會報告。TAP實現應該同步地報告IProgress<T>對象的進度,使得比快速提供進度的非同步實現更廉價,並且允許進度的消費者決定如何以及在哪裡最好地處理資訊(例如進度執行個體本身可以選擇在一個捕獲的同步上下文上收集回呼函數和引發事件)。
IProgreee<T>實現
Progress<T>作為.NET Framework 4.5的一部分,是IProgress<T>的單一實現(未來會提供更多的實現)。Progress<T>聲明如下:
public class Progress<T> : IProgress<T>
{
public Progress();
public Progress(Action<T> handler);
protected virtual void OnReport(T value);
public event EventHandler<T> ProgressChanged;
}
Progress<T>的執行個體公開了一個ProgressChanged事件,它是每次非同步作業報告進度更新的時候觸發。當Progress<T>執行個體被執行個體化時,該事件在被捕獲的同步上下文上觸發(如果沒有上下文可用,那麼用預設的線程池上下文)。控制代碼可能會用這個事件註冊;一個單獨的控制代碼也可能提供給Progress執行個體的建構函式(這純粹是為了方便,就像ProgressChanged 事件的事件控制代碼)。進度更新非同步觸發是為了事件控制代碼執行時避免延遲非同步作業。其他的IProgress<T>實現可能選擇使用了不同的語義。
如何選擇提供的重載函數
有了CancellationToken和IProgress<T>參數,TAP的實現預設有4個重載函數:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
然而,因為它們沒有提供cancellation和progress的能力,許多TAP實現有了最短的重載的需求:
public Task MethodNameAsync(…);
如果一個實現支援cancellation或者progress但不同時支援,那麼TAP實現可以提供2個重載:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
// … or …
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress<T> progress);
如果實現同時支援cancellation和progress,那麼它可以預設提供4個重載。然而,只有2個有效:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
為了得到那2個遺失的重載,開發人員可以通過給CancellationToken參數傳遞CancellationToken.None(或者default(CancellationToken))和/或給progress參數傳遞null。
如果期望TAP方法的每一種用法都應該使用cancellation和/或progress,那麼不接受相關參數的重載可以忽略。
如果TAP方法的多個重載公開了可選的cancellation和/或progress,那麼不支援cancellation和/或progress的重載的表現應該像支援他們的重載已經傳遞了CancellationToken.None和null分別給cancellation和progress一樣。
實現基於Task的非同步模式
產生方法
編譯器產生
在.NET Framework 4.5中,C#編譯器實現了TAP。任何標有async關鍵字的方法都是非同步方法呼叫,編譯器會使用TAP執行必要的轉換從而非同步地實現方法。這樣的方法應該返回Task或者Task<TResult>類型。在後者的案例中,方法體應該返回一個TResult,且編譯器將確保通過返回的Task<TResult>是可利用的。相似地,方法體內未處理的例外狀況會被封送到輸出的task,造成返回的Task以Faulted的狀態結束。一個例外是如果OperationCanceledException(或衍生類別型)未經處理,那麼返回的Task會以Canceled狀態結束。
手動產生
開發人員可以手動地實現TAP,就像編譯器那樣或者更好地控制方法的實現。編譯器依賴來自System.Threading.Tasks命名空間暴露的公開表面地區(和建立在System.Threading.Tasks之上的System.Runtime.CompilerServices中支援的類型),還有對開發人員直接可用的功能。當手動實現TAP方法時,開發人員必須保證當非同步作業完成時,完成返回的Task。
混合產生
在編譯器產生的實現中混合核心邏輯的實現,對於手動實現TAP通常是很有用的。比如這種情況,為了避免方法直接調用者產生而不是通過Task暴露的異常,如:
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
… // code that uses await
}
參數應該在編譯器產生的非同步方法呼叫之外改變,這種委託有用的另一種場合是,當一個“快速通道”最佳化可以通過返回一個緩衝的task來實現的時候。
工作負載
計算受限和I/O受限的非同步作業可以通過TAP方法實現。然而,當TAP的實現從一個庫公開暴露時,應該只提供給包含I/O操作的工作負載(它們也可以包含計算,但不應該只包含計算)。如果一個方法純粹受計算限制,它應該只通過一個非同步實現暴露,消費者然後就可以為了把該任務卸載給其他的線程的目的來選擇是否把那個同步方法的調用封裝成一個Task,並且/或者來實現並行。
計算限制
Task類最適合表示計算密集型操作。預設地,為了提供有效執行操作,它利用了.Net線程池中特殊的支援,同時也對非同步計算何時,何地,如何執行提供了大量的控制。
產生計算受限的tasks有幾種方法。
在.Net 4中,啟動一個新的計算受限的task的主要方法是TaskFactory.StartNew(),該方法接受一個非同步執行的委託(一般來說是一個Action或者一個Func<TResult>)。如果提供了一個Action,返回的Task就代表那個委託的非同步執行操作。如果提供了一個Func<TResult>,就會返回一個Task<TResult>。存在StartNew()的重載,該重載接受CancellationToken,TaskCreationOptions,和TaskScheduler,這些都對task的調度和執行提供了細粒度的控制。作用在當前調度者的工廠執行個體可以作為Task類的靜態屬性,例如Task.Factory.StartNew()。
在.Net 4.5中,Task類型暴露了一個靜態Run方法作為一個StartNew方法的捷徑,可以很輕鬆地使用它來啟動一個作用線上程池上的計算受限的task。從.Net 4.5開始,對於啟動一個計算受限的task,這是一個更受人喜歡的機制。當行為要求更多的細粒度控制時,才直接使用StartNew。
Task類型公開了建構函式和Start方法。如果必須要有分離自調度的建構函式,這些就是可以使用的(正如先前提到的,公開的APIs必須只返回已經啟動的tasks)。
Task類型公開了多個ContinueWith的重載。當另外一個task完成的時候,該方法會建立新的將被調度的task。該重載接受CancellationToken,TaskCreationOptions,和TaskScheduler,這些都對task的調度和執行提供了細粒度的控制。
TaskFactory類提供了ContinueWhenAll 和ContinueWhenAny方法。當提供的一系列的tasks中的所有或任何一個完成時,這些方法會建立一個即將被調度的新的task。有了ContinueWith,就有了對於調度的控制和任務的執行的支援。
思考下面的渲染圖片的非同步方法呼叫。task體可以獲得cancellation token為的是,當渲染髮生的時候,如果一個撤銷請求到達後,代碼可能過早退出。而且,如果一個撤銷請求在渲染開始之前發生,我們也可以阻止任何的渲染。
public Task<Bitmap> RenderAsync(
ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for(int y=0; y<data.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
for(int x=0; x<data.Width; x++)
{
… // render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}
如果下面的條件至少一個是正確的,計算受限的tasks會以一個Canceled狀態的結束:
在Task過度到TaskStatus.Running狀態之前,CancellationToken為一個發出撤銷請求的建立方法的參數提供(如StartNew,Run)。
有這樣的一個Task,它內部有未處理的OperationCanceledException。該OperationCanceledException 包含和CancellationToken屬性同名的CancellationToken傳遞到該Task,且該CancellationToken已經發出了撤銷請求。
如果該Task體中有另外一個未處理的例外狀況,那麼該Task就會以Faulted的狀態結束,同時在該task上等待的任何嘗試或者訪問它的結果都將導致拋出異常。
I/O限制
使用TaskCompletionSource<TResult>類型建立的Tasks不應該直接被全部執行的線程返回。TaskCompletionSource<TResult>暴露了一個返回相關的Task<TResult>執行個體的Task屬性。該task的生命週期通過TaskCompletionSource<TResult>執行個體暴露的方法控制,換句話說,這些執行個體包括SetResult, SetException, SetCanceled, 和它們的TrySet* 變數。
思考這樣的需求,建立一個在特定的時間之後會完成的task。比如,當開發人員在UI情境中想要延遲一個活動一段時間時,這可能使有用的。.NET中的System.Threading.Timer類已經提供了這種能力,在一段特定時間後非同步地調用一個委託,並且我們可以使用TaskCompletionSource<TResult>把一個Task放在timer上,例如:
public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource<DateTimeOffset>();
new Timer(self =>
{
((IDisposable)self).Dispose();
tcs.TrySetResult(DateTimeOffset.UtcNow);
}).Change(millisecondsTimeout, -1);
return tcs.Task;
}
在.Net 4.5中,Task.Delay()就是為了這個目的而生的。比如,這樣的一個方法可以使用到另一個非同步方法呼叫的內部,以實現一個非同步輪訓迴圈:
public static async Task Poll(
Uri url,
CancellationToken cancellationToken,
IProgress<bool> progress)
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
沒有TaskCompletionSource<TResult>的非泛型副本。然而,Task<TResult>派生自Task,因而,泛型的TaskCompletionSource<TResult>可以用於那些 I/O受限的方法,它們都利用一個假的TResult源(Boolean是預設選擇,如果開發人員關心Task向下轉型的Task<TResult>的消費者,那麼可以使用一個私人的TResult類型)僅僅返回一個Task。比如,開發的之前的Delay方法是為了順著產生的Task<DateTimeOffset>返回當前的時間。如果這樣的 一個結果值是不必要的,那麼該方法可以通過下面的代碼取而代之(注意傳回型別的改變和TrySetresult參數的改變):
public static Task Delay(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource<bool>();
new Timer(self =>
{
((IDisposable)self).Dispose();
tcs.TrySetResult(true);
}).Change(millisecondsTimeout, -1);
return tcs.Task;
}
混合計算限制和I/O限制的任務
非同步方法呼叫不是僅僅受限於計算受限或者I/O受限的操作,而是可以代表這兩者的混合。實際上,通常情況是不同性質的多個非同步作業被組合在一起產生更大的混合操作。比如,思考之前的RenderAsync方法,該方法基於一些輸入的ImageData執行一個計算密集的操作來渲染一張圖片。該ImageData可能來自於一個我們非同步訪問的Web服務:
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
這個例子也展示了一個單獨的CancellationToken是如何通過多個非同步作業被線程化的。