工作運算式

本文說明 F# 對於工作運算式的支援;該運算式類似於非同步運算式,但可讓您直接撰寫 .NET 工作。 如同非同步運算式,工作運算式也會以非同步方式執行程式碼,也就是說,不會妨礙其他工作的執行。

非同步程式碼通常會使用非同步運算式來撰寫。 在與用來建立或取用 .NET 工作的 .NET 程式庫進行大量交互操作時,建議最好使用工作運算式。 工作運算式也可改善效能和偵錯體驗。 不過,工作運算式受到某些限制,本文稍後會加以說明。

語法

task { expression }

在先前的語法中,expression 所代表的計算設定為以 .NET 工作的形式執行。 工作會在此程式碼執行後立即啟動,並在目前的執行緒上執行,直到其第一個非同步作業執行 (例如非同步睡眠、非同步 I/O,或其他基本非同步作業)。 運算式的型別為 Task<'T>,其中,'T 是使用 return 關鍵字時由運算式傳回的型別。

使用 let! 進行繫結

在工作運算式中,有些運算式和作業是同步的,有些則是非同步的。 在等候非同步作業的結果時應使用 let!,而不是一般 let 繫結。 let! 的效果是在計算執行時,其他的計算或執行緒仍得以繼續執行。 在 let! 繫結的右側傳回後,工作的其餘部分會繼續執行。

下列程式碼顯示 letlet! 之間的差異。 使用 let 的程式碼會建立物件形式的工作;後續您可以使用 task.Wait()task.Result (或其他指令) 予以等候。 使用 let! 的程式碼會啟動工作並等候其結果。

// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int)  = stream.ReadAsync(buffer, offset, count, cancellationToken)

F# task { } 運算式可以等候下列類型的非同步作業:

return 運算式

在工作運算式中,return expr 用來傳回工作的結果。

return! 運算式

在工作運算式中,return! expr 用來傳回另一項工作的結果。 這相當於使用 let!,然後立即傳回結果。

控制流程

工作運算式可包含控制流程建構 for .. in .. dowhile .. dotry .. with ..try .. finally ..if .. then .. elseif .. then ..。 這些項目可能進一步包含其他工作建構,(會同步執行的 withfinally 處理常式除外)。 如果您需要非同步 try .. finally ..,請使用 use 繫結搭配 IAsyncDisposable 型別的物件。

useuse! 繫結

在工作運算式中,use 繫結可繫結至 IDisposableIAsyncDisposable 型別的值。 針對後者,會以非同步方式執行處置清除作業。

除了 let! 以外,您還可以使用 use! 來執行非同步繫結。 let!use! 間的差異與 letuse 間的差異相同。 針對 use!,物件會在目前範圍關閉時遭處置。 請注意,在 F# 6 中,use! 不允許將值初始化為 Null (儘管 use 允許)。

值工作

值工作是用來避免在工作型程式設計中進行配置的結構。 值工作是一種臨時值,可使用 .AsTask() 轉換為實際工作。

若要從工作運算式建立值工作,請使用 |> ValueTask<ReturnType>|> ValueTask。 例如:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

新增取消權杖和取消檢查

不同於 F# 非同步運算式,工作運算式不會隱含地傳遞取消權杖,也不會隱含地執行取消檢查。 如果您的程式碼需要取消權杖,您應將取消權杖指定為參數。 例如:

open System.Threading

let someTaskCode (cancellationToken: CancellationToken) =
    task {
        cancellationToken.ThrowIfCancellationRequested()
        printfn $"continuing..."
    }

如果您想要正確地將程式碼設為可取消,請仔細檢查您是否將取消權杖傳遞至所有支援取消的 .NET 程式庫作業。 例如,Stream.ReadAsync 有多個多載,其中一個接受取消權杖。 如果不使用此多載,該非同步讀取作業就會無法取消。

背景工作

根據預設,.NET 工作會使用 SynchronizationContext.Current (如果有的話) 來排程。 這可讓工作成為在使用者介面執行緒上執行的合作式交錯代理程式,而不會封鎖 UI。 如果沒有的話,則會將工作排程到 .NET 執行緒集區繼續執行。

實際上,產生工作的程式庫程式碼最好會忽略同步處理內容,並且一律切換至 .NET 執行緒集區 (如有需要)。 您可以使用 backgroundTask { } 來達成此目的:

backgroundTask { expression }

背景工作會基於下列原則忽略任何 SynchronizationContext.Current:如果在具有非 Null SynchronizationContext.Current 的執行緒上啟動,則會使用 Task.Run 切換至執行緒集區中的背景執行緒。 如果在具有 Null SynchronizationContext.Current 的執行緒上啟動,則會在該執行緒上執行。

注意

實際上,這表示在 F# 工作程式碼中通常不需要呼叫 ConfigureAwait(false)。 您應使用 backgroundTask { ... } 撰寫要在背景中執行的工作。 任何對背景工作的外部工作繫結,都會在背景工作完成時重新同步處理至 SynchronizationContext.Current

tailcall 相關工作的限制

不同於 F# 非同步運算式,工作運算式不支援 tailcall。 也就是說,在 return! 執行時,目前的工作會註冊為等候結果傳回的工作。 這表示使用工作運算式實作的遞迴函式和方法可能會建立無界限的工作鏈結,而這些工作可能會使用無界限的堆疊或堆積。 例如,請考慮下列程式碼:

let rec taskLoopBad (count: int) : Task<string> =
    task {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! taskLoopBad (count-1)
    }

let t = taskLoopBad 10000000
t.Wait()

此編碼樣式不應與工作運算式搭配使用 — 會建立 10000000 個工作的鏈結,並造成 StackOverflowException。 如果在每個迴圈叫用上新增非同步作業,程式碼將使用基本上無界限的堆積。 請考慮讓此程式碼改用明確的迴圈,例如:

let taskLoopGood (count: int) : Task<string> =
    task {
        for i in count .. 1 do
            printfn $"looping... count = {count}"
        return "done!"
    }

let t = taskLoopGood 10000000
t.Wait()

如需執行非同步 tailcalls,請使用支援 tailcall 的 F# 非同步運算式。 例如:

let rec asyncLoopGood (count: int) =
    async {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! asyncLoopGood (count-1)
    }

let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()

工作實作

工作可使用「可繼續程式碼」(F# 6 的新功能) 來實作。 F# 編譯器會將工作編譯為「可繼續狀態機器」。 相關詳細說明請見可繼續程式碼 RFCF# 編譯器社群研討會

另請參閱