共用方式為


實現以工作為基礎的異步模式

您可以透過三種方式實作以工作為基礎的異步模式(TAP):在 Visual Studio 中使用 C# 和 Visual Basic 編譯程式、手動或透過編譯程式和手動方法的組合。 下列各節會詳細討論每個方法。 您可以使用 TAP 模型來實作計算密集型和 I/O 密集型非同步操作。 [工作負載] 區段會討論每種作業類型。

產生 TAP 方法

使用編譯程式

從 .NET Framework 4.5 開始,使用 async 關鍵詞屬性的任何方法(Async 在 Visual Basic 中)都會被視為異步方法,而 C# 和 Visual Basic 編譯程式會執行必要的轉換,以使用 TAP 以異步方式實作方法。 異步方法應該傳回 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> 物件。 針對後者,函式的主體應該會傳回 TResult,而編譯程式可確保此結果可透過產生的工作物件取得。 同樣地,任何在方法內未處理的例外狀況都會被封送到輸出任務,並導致產生的任務以 TaskStatus.Faulted 狀態結束。 此規則的例外狀況是當 OperationCanceledException (或衍生類型) 未處理時,在此情況下,產生的工作會以 TaskStatus.Canceled 狀態結束。

手動生成 TAP 方法

您可以手動實作 TAP 模式,以更好地控制實作。 編譯器依賴於由 System.Threading.Tasks 命名空間公開的公共表面區域,以及由 System.Runtime.CompilerServices 命名空間中的支持類型。 若要自行實作 TAP,您可以建立 TaskCompletionSource<TResult> 對象、執行異步作,並在完成時呼叫 SetResultSetExceptionSetCanceled 方法,或 Try 其中一個方法的版本。 當您手動實作 TAP 方法時,您必須在所代表的非同步操作完成時完成相應的任務。 例如:

public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
    var tcs = new TaskCompletionSource<int>();
    stream.BeginRead(buffer, offset, count, ar =>
    {
        try { tcs.SetResult(stream.EndRead(ar)); }
        catch (Exception exc) { tcs.SetException(exc); }
    }, state);
    return tcs.Task;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte,
                         offset As Integer, count As Integer,
                         state As Object) As Task(Of Integer)
    Dim tcs As New TaskCompletionSource(Of Integer)()
    stream.BeginRead(buffer, offset, count, Sub(ar)
                                                Try
                                                    tcs.SetResult(stream.EndRead(ar))
                                                Catch exc As Exception
                                                    tcs.SetException(exc)
                                                End Try
                                            End Sub, state)
    Return tcs.Task
End Function

混合式方法

您可能會發現手動實作 TAP 模式很有用,但將實作的核心邏輯委派給編譯程式。 例如,當您想要在編譯程式產生的異步方法外部驗證自變數,讓例外狀況可以逸出至方法的直接呼叫端,而不是透過 System.Threading.Tasks.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 goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

    ' code that uses await goes here

    return value
End Function

另一個這類委派很有用的情況是,當您實作快速路徑優化並想要傳回快取的工作時。

工作負載

您可以實作計算密集型和 I/O 密集型的非同步操作,來作為 TAP 方法。 不過,當 TAP 方法從程式庫公開時,應該只針對涉及 I/O 密集運算的工作負載提供它們(它們也可能涉及計算,但不應該純粹是計算)。 如果方法純粹是計算密集型,則僅應公開為同步實作。 取用它的程式碼可以選擇是否將該同步方法的調用包裝成一個任務,以便將工作分配到另一個執行緒上,或實現平行處理。 如果方法是 I/O 受限,則應該只公開為非同步實作。

計算密集型任務

類別 System.Threading.Tasks.Task 非常適合用來表示需要大量運算的作業。 根據預設,它會利用 類別內 ThreadPool 的特殊支援來提供有效率的執行,同時也能大幅控制異步計算的執行時機、地點和方式。

您可以透過下列方式產生計算系結工作:

  • 在 .NET Framework 4.5 和更新版本中(包括 .NET Core 和 .NET 5+),使用靜態 Task.Run 方法做為 的 TaskFactory.StartNew快捷方式。 Run 您可以輕鬆啟動以執行緒池為目標的計算密集型工作。 這是啟動計算系結工作的慣用機制。 只有在您想要更精細地控制工作時,才直接使用 StartNew

  • 在 .NET Framework 4 中,使用 TaskFactory.StartNew 方法,此方法接受異步執行的委派(通常是 Action<T>Func<TResult>)。 如果您提供 Action<T> 委派,此方法會傳回一個 System.Threading.Tasks.Task 物件,代表該委派的異步執行。 如果您提供 Func<TResult> 委派,此方法會傳回一個 System.Threading.Tasks.Task<TResult> 物件。 StartNew 方法的多載會接受取消標記(CancellationToken)、工作建立選項(TaskCreationOptions)以及工作排程器(TaskScheduler),所有這些都能對工作的排程和執行提供更精細的控制。 以目前工作排程器為目標的處理站實例可作為 類別的Factory靜態屬性(Task)使用,例如:Task.Factory.StartNew(…)

  • 如果您想要分別產生和排程工作任務,請使用 Task 型別的建構函式和 Start 方法。 公用方法只能傳回已啟動的工作。

  • 使用 Task.ContinueWith 方法的多載版本。 此方法會在另一個工作完成時創建並排程一個新工作。 有些 ContinueWith 多載會接受取消標記、接續選項和工作排程器,以便更妥善地控制接續工作的排程和執行。

  • 使用TaskFactory.ContinueWhenAllTaskFactory.ContinueWhenAny 方法。 這些方法會建立一個新工作,當提供的工作集中的所有或任何一個完成時,即開始進行排程。 這些方法也會提供多載來控制這些工作的排程和執行。

在計算系結工作中,如果系統在開始執行工作之前收到取消要求,系統可能會防止執行排程的工作。 因此,如果您提供取消令牌(CancellationToken 物件),您可以將該令牌傳遞至監視令牌的異步程序代碼。 您也可以將令牌提供給上述其中一個方法,例如 StartNewRun ,讓 Task 運行時間也可以監視令牌。

例如,請考慮呈現影像的異步方法。 工作函數的主體可以檢查取消令牌,這樣當渲染期間收到取消請求時,程式碼能夠提前結束。 此外,如果取消要求在轉譯開始之前到達,您會想要防止轉譯作業:

internal 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);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
                            CancellationToken) As Task(Of Bitmap)
    Return Task.Run(Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 to data.Height - 1
                            cancellationToken.ThrowIfCancellationRequested()
                            For x As Integer = 0 To data.Width - 1
                                ' render pixel [x,y] into bmp
                            Next
                        Next
                        Return bmp
                    End Function, cancellationToken)
End Function

如果至少有下列其中一個條件成立,計算密集型工作會以Canceled狀態結束:

  • 取消要求會透過 CancellationToken 物件抵達,該物件會在工作轉換為StartNew狀態之前,提供做為建立方法的自變數(例如 RunRunning)。

  • 例外 OperationCanceledException 狀況在這類工作的主體內未處理,該例外狀況包含傳遞至工作的相同 CancellationToken 例外狀況,且該令牌會顯示要求取消。

如果在工作體中有其他例外狀況未被處理,工作會以Faulted狀態結束,而且任何等待工作或存取其結果的嘗試都會拋出例外狀況。

I/O 受限任務

若要建立不應該由線程直接支援整個執行的工作,請使用 TaskCompletionSource<TResult> 類型。 此類型會公開屬性,該屬性傳回相關聯的Task實例。 此工作的生命週期是由 TaskCompletionSource<TResult> 方法,包括 SetResultSetExceptionSetCanceled 和其 TrySet 變體,所控制。

假設您想要建立將在指定時段後完成的工作。 例如,您可能想要延遲使用者介面中的活動。 類別 System.Threading.Timer 已經提供在指定時段後以異步方式調用委派的功能,而且您可以使用 TaskCompletionSource<TResult> ,在計時器上增加 Task<TResult>,例如:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    TaskCompletionSource<DateTimeOffset> tcs = null;
    Timer timer = null;

    timer = new Timer(delegate
    {
        timer.Dispose();
        tcs.TrySetResult(DateTimeOffset.UtcNow);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<DateTimeOffset>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
    Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(DateTimeOffset.UtcNow)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

方法 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);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            await DownloadStringAsync(url)
            success = true
        Catch
            ' ignore errors
        End Try
        progress.Report(success)
    Loop
End Function

類別TaskCompletionSource<TResult>沒有非泛型對應版本。 不過, Task<TResult> 衍生自 Task,因此您可以針對只傳回工作的 I/O 系結方法使用泛型 TaskCompletionSource<TResult> 物件。 若要達成此目標,您可以使用一個帶有虛擬TResult的來源(Boolean 是一個不錯的預設選擇,但如果您擔心 Task 使用者會將它向下轉型為 Task<TResult>,您可以改用私有的 TResult 類型)。 例如, Delay 上一個範例中的方法會傳回目前的時間以及產生的位移 (Task<DateTimeOffset>)。 如果不需要這類結果值,方法可以改為編碼如下(請注意傳回型別的變更,並將自變數變更為 TrySetResult):

public static Task<bool> Delay(int millisecondsTimeout)
{
     TaskCompletionSource<bool> tcs = null;
     Timer timer = null;

     timer = new Timer(delegate
     {
         timer.Dispose();
         tcs.TrySetResult(true);
     }, null, Timeout.Infinite, Timeout.Infinite);

     tcs = new TaskCompletionSource<bool>(timer);
     timer.Change(millisecondsTimeout, Timeout.Infinite);
     return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
    Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
    Dim timer As Timer = Nothing

    Timer = new Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(True)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of Boolean)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

混合計算密集與 I/O 密集的工作

異步方法不限於計算系結或 I/O 系結作業,但可能代表兩者混合。 事實上,多個異步作業通常會合併成較大的混合作業。 例如, RenderAsync 上一個範例中的方法會執行大量運算,以根據某些輸入 imageData來轉譯影像。 這可能來自您以非同步方式存取的網路服務:

public async Task<Bitmap> DownloadDataAndRenderImageAsync(
    CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
             cancellationToken As CancellationToken) As Task(Of Bitmap)
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

此範例也示範了如何使用單個取消令牌來控制多個異步操作。 如需詳細資訊,請參閱 取用以工作為基礎的異步模式中的取消使用方式一節。

另請參閱