本文說明 F# 中的工作表示式支援,其類似於 異步表達式 ,但可讓您直接撰寫 .NET 工作。 如同異步表達式,工作運算式會以異步方式執行程序代碼,也就是不會封鎖執行其他工作。
異步程式代碼通常會使用異步表達式撰寫。 當與建立或使用 .NET 任務的 .NET 程式庫廣泛互操作時,最好使用任務表達式。 工作表達式也可以改善效能和偵錯體驗。 不過,工作表達式隨附一些限制,本文稍後會說明這些限制。
語法
task { expression }
在先前的語法中,所 expression 表示的計算會設定為以 .NET 工作的形式執行。 此工作會在執行此程序代碼後立即啟動,並在目前的線程上執行,直到第一次異步操作開始(例如異步睡眠、異步 I/O 或其他基本異步操作)。 表達式的類型為 Task<'T>,其中 'T 是使用 return 關鍵詞時表達式所傳回的類型。
使用 let! 進行繫結
在工作表達式中,有些表達式和作業是同步的,有些是異步的。 當您等待異步操作的結果時,與其使用一般的 let 綁定,不如使用 let!。
let! 的效果是使得在執行計算時,可以同時進行其他計算或執行線程。 系結右側 let! 傳回之後,其餘的工作會繼續執行。
下列程式代碼顯示 let 與 let!之間的差異。 使用 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 { } 運算式可以等待下列類型的異步作業:
- .NET 任務、Task<TResult> 和非泛型的 Task。
- .NET 值型任務 ValueTask<TResult> 和非泛型 ValueTask。
- F# 異步計算
Async<T>。 - 在 F# RFC FS-1097 中指定的 “GetAwaiter” 模式之後的任何物件。
return 運算式
在工作表達式中, return expr 用來傳回工作的結果。
return! 運算式
在工作表達式內, return! expr 用來傳回另一個工作的結果。 它相當於使用 let! ,然後立即傳回結果。
流程控制
工作運算式可以包含控制流程建構 for .. in .. do、 while .. do、、 try .. with ..、 try .. finally ..、 if .. then .. else和 if .. then ..。 這些可能會包含進一步的工作建構,但 with 與 finally 處理程式除外,這些建構會以同步方式執行。 如果您需要異步try .. finally ..,請使用use綁定配合類型為IAsyncDisposable的物件。
use 和 use! 系結
在工作表達式中,use 系結可以連接至 IDisposable 或 IAsyncDisposable 類型的值。 針對後者,會以異步方式執行處置清除作業。
除了 let!之外,您還可以使用 use! 來執行異步系結。
let! 與 use! 之間的差異與 let 與 use之間的差異相同。 針對 use!,物件會在目前範圍結束時被處置。 請注意,在 F# 6 中, use! 不允許將值初始化為空值,然而 use 則可以。
open System
open System.IO
open System.Security.Cryptography
task {
// use IDisposable
use httpClient = new Net.Http.HttpClient()
// use! Task<IDisposable>
use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
// use IDisposable
use aes = Aes.Create()
aes.KeySize <- 256
aes.GenerateIV()
aes.GenerateKey()
// do! Task
do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")
// use IAsyncDisposable
use outputStream = File.Create "secret.enc"
// use IDisposable
use encryptor = aes.CreateEncryptor()
// use IAsyncDisposable
use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
// do! Task
do! exampleDomain.Content.CopyToAsync cryptoStream
}
有價值的任務
值任務是用來避免在工作型程序設計中記憶體分配的結構體。 值工作是短暫的值,可以使用 .AsTask() 來轉換成實際的工作。
若要從工作運算式建立值工作,請使用 |> ValueTask<ReturnType> 或 |> ValueTask。 例如:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and! 系結 (從 F# 10 開始)
在工作運算式中,可以同時等候多個異步作 (Task<'T>、 ValueTask<'T>Async<'T> 等) 。 比較:
// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
task {
let! x = getX()
let! y = getY()
return x, y
}
// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
task {
let! x = getX()
and! y = getY()
return x, y
}
新增取消令牌和取消檢查
不同於 F# 異步表示式,工作運算式不會隱含傳遞取消令牌,也不會隱含執行取消檢查。 如果您的程式代碼需要取消令牌,您應該將取消令牌指定為參數。 例如:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
如果您想要正確地實現可取消的程式碼,請仔細檢查是否將取消令牌傳遞到所有支援取消的 .NET 程式庫作業。 例如,Stream.ReadAsync 有多個重載,其中一個版本接受取消令牌。 如果您未使用此多載,該特定異步讀取作業將無法取消。
背景任務
根據預設,如果存在,則會使用 SynchronizationContext.Current 排程 .NET 工作。 這可讓工作以協作交錯的方式作為在使用者介面執行緒上運行的代理,而不會阻塞UI。 如果不存在,則工作繼續會被排程到 .NET 執行緒集區。
實際上,產生工作的函式庫程式碼通常會忽略同步處理內容,並在需要時始終切換至 .NET 執行緒池。 您可以使用backgroundTask { }來達成此目的。
backgroundTask { expression }
背景任務會在以下意義上忽略任何 SynchronizationContext.Current:如果在具有非 Null SynchronizationContext.Current 的執行緒上啟動,則會使用 Task.Run 切換至執行緒池中的背景執行緒。 如果在具有 null SynchronizationContext.Current的線程上啟動,它會在該相同的線程上執行。
備註
實際上,這表示在 F# 工作程式代碼中通常不需要呼叫 ConfigureAwait(false) 。 相反地,打算在背景中執行的工作應該使用 backgroundTask { ... }來撰寫。 任何與背景工作相關的外部任務繫結,會在背景工作完成時重新同步至 SynchronizationContext.Current。
有關 tailcalls 的工作限制
不同於 F# 異步表示式,任務表示式不支援尾端呼叫。 也就是說,當執行 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()
此程式代碼撰寫樣式不應與工作運算式搭配使用,它會建立100000000個工作的鏈結,並造成 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()
如果需要異步尾遞迴,請使用支援尾遞迴的 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# 編譯程式編譯成「可繼續的狀態機器」。 這些會在 可繼續的程式代碼 RFC 和 F# 編譯程式社群會話中詳細說明。