訓練
學習路徑
F# 是一種開放原始碼且跨平台的程式設計語言,可讓您輕鬆地撰寫簡潔、高效能、穩固且實用的程式碼。 其為一般用途語言,可讓您建立許多不同類型的應用程式,例如 Web API、桌面、IoT、遊戲等。
非同步程式設計因為許多不同的原因,而成為現代應用程式不可或缺的機制。 大多數開發人員會遇到兩個主要的使用案例:
雖然背景工作通常涉及多個執行緒的使用率,但是請務必將非同步和多執行緒的概念分開考量。 事實上,它們是兩個不同的概念,彼此互不相關。 本文詳細說明這兩個截然不同的概念。
上一點提到非同步與多個執行緒的使用率無關,現在我們要進一步說明這個部分。 以下介紹三個概念,它們有時相關,但嚴格來說,卻又差異甚大:
雖然這三個都是正交的概念,但很容易混淆,尤其是在同時使用的時候。 舉例來說,您可能需要平行執行多個非同步計算。 此關聯性不代表平行處理原則或非同步彼此相關。
如果您細想「非同步」(asynchronous) 的詞源,會發現此字包含兩個部分:
如果您將這兩個詞彙放在一起時,您會發現「asynchronous」代表「不同時」。 介紹完畢 此定義並未涉及並行或平行處理原則, 在實務上也是如此。
就實際情況而言,系統會將 F# 中非同步計算的執行時間與主程式流程的執行時間分開。 獨立執行不代表並行或平行處理原則,也不代表計算總是在背景執行。 事實上,非同步計算甚至可以同步執行,這取決於計算的性質以及執行計算所在的環境。
本文的最大重點是讓您了解非同步計算與主程式流程無關。 雖然沒有什麼確切的定論說明應執行非同步計算的時機或方式,但是有一些方法可以協調和排程非同步計算。 本文的其餘部分將探討 F# 非同步的核心概念,以及如何使用 F# 內建的型別、語言函式和運算式。
在 F# 中,非同步程式設計是以兩個核心概念為中心,包含非同步計算和工作。
async { }
運算式的 Async<'T>
型別代表可組合的非同步計算,啟動後可形成工作。task { }
運算式的 Task<'T>
型別代表執行中的 .NET 工作。一般而言,如果與使用工作的 .NET 程式庫交互操作,而且不依賴非同步程式碼尾呼叫或隱含取消語彙基元傳播,則應該考慮在新的程式碼中使用 task {…}
,而不是使用 async {…}
。
您可以在下列範例中看到「非同步」程式設計的基本概念:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
在此範例中,printTotalFileBytesUsingAsync
語言函式的型別為 string -> Async<unit>
。 事實上,呼叫語言函式並不會執行非同步計算。 相反地,此動作會傳回 Async<unit>
,可作為非同步執行工作的規格。 此動作會在其主體中呼叫 Async.AwaitTask
,以將 ReadAllBytesAsync 的結果轉換成適當的型別。
另一個重要的程式碼是呼叫 Async.RunSynchronously
。 這是其中一個非同步模組的 starting 語言函式,如果您想要 F# 的非同步計算確實執行,就必須呼叫此語言函式。
這是與 async
程式設計 C#/Visual Basic 樣式的基本差異。 在 F# 中,可以將非同步計算視為極非經常性存取工作。 必須明確啟動這些工作才能確實執行。 這個動作有一些好處,可以讓您在 C# 或 Visual Basic 中更輕鬆地合併和排序非同步工作。
以下範例結合不同的計算,並以前一個範例為基礎:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
如您所見,main
語言函式有更多元素。 就概念上而言,此語言函式會執行下列動作:
Seq.map
將命令列引數轉換成 Async<unit>
計算的序列。Async<'T[]>
,讓它排程 printTotalFileBytes
計算的執行時間,使其與自身平行執行。Async<unit>
,也就是 unit[]
。Async.RunSynchronously
明確執行整體的組成計算,在執行完成前將遭到封鎖。此程式執行時,printTotalFileBytes
會針對每個命令列引數平行執行。 由於執行非同步計算不需仰賴程式流程,所以並無明確規定這些計算應先列印其資訊或完成執行流程。 系統會將這些計算排程在同一時間執行,但不保證執行順序。
Async<'T>
是工作規格而非已經在執行的工作,所以您可以輕鬆執行更複雜的轉換。 下方範例為一組非同步計算中的各個計算作業排列順序,讓這些計算得以接連執行。
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
這會讓 printTotalFileBytes
依照 argv
的元素順序來執行計算,而非以平行方式執行。 系統會在前一個計算執行完成後才排程每個接續作業執行的時間,因此要確保每個計算作業並無彼此重疊。
當您在 F# 中撰寫非同步程式碼時,您通常會和為您處理計算排程的架構互動。 不過情況並非總是如此,所以最好了解可用來排程非同步工作的各種語言函式。
F# 非同步計算是工作的規格,而非已經在執行的工作,所以必須以 starting 語言函式來明確啟動。 有許多在不同情境下都很有幫助的非同步啟動方法 (英文)。 下一節會說明一些較常見的 starting 語言函式。
在非同步計算中啟動子計算。 這會同時執行多個非同步計算。 子計算與父計算共用取消語彙基元, 因此取消父計算,也會取消子計算。
簽名:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
使用時機:
需注意下列情況:
Async.StartChild
啟動多個計算以及將這些計算排程為平行執行,這兩者並不相同。 如果您想要平行排程計算,請使用 Async.Parallel
。可執行非同步計算,並立即在目前的作業系統執行緒上啟動。 如果您需要在計算期間更新呼叫執行緒上的某些項目,這個函式就非常實用。 例如,如果非同步計算必須更新 UI (例如更新進度列),則應使用 Async.StartImmediate
。
簽名:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
使用時機:
需注意下列情況:
Async.StartImmediate
。在執行緒集區中執行計算。 傳回 Task<TResult>。它會在計算終止時 (會產生結果、擲回例外狀況或遭到取消) 完成,且具有與計算對應的狀態。 如果未提供取消語彙基元,將使用預設的取消語彙基元。
簽名:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
使用時機:
需注意下列情況:
Task
物件,如果經常使用可能會增加負擔。這個函式會提欲平行執行的非同步計算排列順序,並依照系統提供各個結果的順序暫止結果陣列。 指定 maxDegreeOfParallelism
參數,可以選擇調整或節流平行處理原則的程度。
簽名:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
使用時機:
需注意下列情況:
依照非同步計算傳遞的順序排程這些計算執行的順序。 系統會先執行第一個計算,再執行下一個,以此類推, 不會平行執行任何計算。
簽名:
computations: seq<Async<'T>> -> Async<'T[]>
使用時機:
需注意下列情況:
此語言函式會傳回非同步計算,此計算會等候指定的 Task<TResult> 完成,並將結果以 Async<'T>
傳回
簽名:
task: Task<'T> -> Async<'T>
使用時機:
需注意下列情況:
此語言函式會建立非同步計算,該計算會執行指定的 Async<'T>
,並傳回 Async<Choice<'T, exn>>
。 如果指定的 Async<'T>
順利完成,將傳回 Choice1Of2
與結果值。 如果在完成之前,系統擲回例外狀況,則會傳回 Choice2of2
與引發的例外狀況。 如果針對許多計算所組成的非同步計算組使用此語言函式,且其中一個計算擲回例外狀況,該計算組中包含的計算將完全停止。
簽名:
computation: Async<'T> -> Async<Choice<'T, exn>>
使用時機:
需注意下列情況:
此語言函式會建立非同步計算,該計算雖然會執行指定的計算,但會捨棄結果。
簽名:
computation: Async<'T> -> Async<unit>
使用時機:
ignore
語言函式類似。需注意下列情況:
Async.Start
,或另一個需要 Async<unit>
的語言函式,而必須使用 Async.Ignore
,請考慮是否可以接受捨棄結果。 請不要為了符合型別特徵標記而捨棄結果。此語言函式會執行非同步計算,並等候呼叫執行緒的結果。 如果計算暫止例外狀況,便會傳播例外狀況。 此呼叫正在封鎖。
簽名:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
使用時機:
需注意下列情況:
Async.RunSynchronously
會封鎖呼叫執行緒,直到執行完成。此語言函式可啟動會在執行緒集區中傳回 unit
的非同步計算, 不會等候該計算完成和/或觀察例外狀況的結果。 透過 Async.Start
來啟動的巢狀計算與呼叫這些計算的父計算,兩者為分開啟動。巢狀計算的存留期不受任何父計算的影響, 因此取消父計算,不會使任何子計算遭到取消。
簽名:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
只在以下情況使用:
需注意下列情況:
Async.Start
啟動的計算所引發的例外狀況不會傳播給呼叫者。 系統將完全回溯呼叫堆疊。Async.Start
啟動的工作 (例如呼叫 printfn
) 都不會對程式執行的主執行緒造成影響。如果要使用 async { }
程式設計,您可能需要與 .NET 程式庫或 C# 程式碼基底交互操作。這些程式庫或程式碼基底使用 async/await 樣式的非同步程式設計。 由於 C# 和大部分的 .NET 程式庫都使用 Task<TResult> 和 Task 型別作為其核心抽象概念,因此可能會變更您撰寫 F# 非同步程式碼的方式。
一種選項是改為直接使用 task { }
來撰寫 .NET 工作。 您也可以使用 Async.AwaitTask
語言函式來等候 .NET 非同步計算:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
您可以使用 Async.StartAsTask
語言函式將非同步計算傳遞至 .NET 呼叫者:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
若要運用使用 Task (即不傳回值的 .NET 非同步計算) 的 API,您可能需要新增可以將 Async<'T>
轉換成 Task 的語言函式:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
已經有可以接受 Task 作為輸入的 Async.AwaitTask
。 透過此語言函式和先前定義的 startTaskFromAsyncUnit
語言函式,您可以在 F# 非同步計算中啟動和等候 Task 型別。
您可以在 F# 中直接使用 task { }
來撰寫工作,例如:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
在此範例中,printTotalFileBytesUsingTasks
語言函式的型別為 string -> Task<unit>
。 呼叫語言函式會開始執行工作。
系統會等到工作完成再呼叫 task.Wait()
。
雖然本文各處都提到執行緒,但最重要的是以下兩點:
例如,視工作的性質而定,計算可能真的會在其呼叫者的執行緒上執行。 計算也可以在執行緒之間「跳躍」,這讓計算可以借用執行緒的一小段時間,以在各個「等候」(例如在網路呼叫傳輸時) 期間之間的空檔執行有用的工作。
雖然 F# 提供了一些在目前執行緒上 (或明確可知不在目前執行緒上) 啟動非同步計算的功能,但非同步通常與特定的執行緒策略無關。
訓練
學習路徑
F# 是一種開放原始碼且跨平台的程式設計語言,可讓您輕鬆地撰寫簡潔、高效能、穩固且實用的程式碼。 其為一般用途語言,可讓您建立許多不同類型的應用程式,例如 Web API、桌面、IoT、遊戲等。