トレーニング
ラーニング パス
F# は、オープンソースのクロスプラットフォーム プログラミング言語であり、簡潔でパフォーマンスが高く、堅牢で実用的なコードを簡単に記述できます。 これは、Web API、デスクトップ、IoT、ゲームなどの、数多くのさまざまな種類のアプリケーションを作成できる汎用言語です。
このブラウザーはサポートされなくなりました。
Microsoft Edge にアップグレードすると、最新の機能、セキュリティ更新プログラム、およびテクニカル サポートを利用できます。
非同期プログラミングは、さまざまな理由で、最新のアプリケーションに不可欠なメカニズムです。 ほとんどの開発者が直面する主なユース ケースが 2 つあります。
多くの場合、バックグラウンド作業では複数のスレッドを使用する必要がありますが、非同期性とマルチスレッドの概念を別個に考えることが重要です。 実際に、それらは別個の考慮事項であり、一方によってもう一方が暗示されることはありません。 この記事では、別個の概念について詳しく説明します。
前の点 (非同期性は複数スレッドの利用と無関係であること) について、もう少し詳しく説明しておく価値があります。 関連することがあるものの、厳密には互いに独立している概念が 3 つあります。
この 3 つは直交する概念ですが、一緒に使用される場合には特に融合されやすい可能性があります。 たとえば、複数の非同期計算を並列に実行することが必要になる場合があります。 このリレーションシップは、並列性または非同期性が互いを暗示することを意味するものではありません。
"asynchronous" (非同期) という単語の語源を考えると、2 つの部分が含まれます。
この 2 つの用語をまとめると、"asynchronous" (非同期) とは "not at the same time" (同時ではない) ことを意味します。 これで完了です。 この定義には、コンカレンシーまたは並列処理という意味はありません。 これは現実にも当てはまります。
現実には、F# での非同期計算は、メイン プログラム フローとは別に実行するようにスケジュールされています。 この独立した実行は、コンカレンシーまたは並列処理を意味しません。また、計算が常にバックグラウンドで行われることも意味しません。 実際に、非同期計算は、計算の性質と計算が実行されている環境に応じて、同期的に実行することもできます。
主要な結論として、非同期計算はメインのプログラム フローに依存しません。 非同期計算を実行するタイミングと方法についての保証はほとんどありませんが、調整とスケジュール設定にはいくつかのアプローチがあります。 この記事の残りの部分では、F# の非同期性の主要な概念と、F# に組み込まれている型、関数、および式の使用方法について説明します。
F# では、非同期プログラミングは、非同期計算とタスクという 2 つの主要な概念を中心にしています。
async { }
式を含む Async<'T>
型。task { }
式を含む Task<'T>
型。一般に、タスクを使用する .NET ライブラリを相互運用している場合や、非同期コード tailcalls や暗黙的なキャンセル トークンの伝達に依存していない場合は、新しいコードで async {…}
よりも task {…}
を使用することを検討する必要があります。
次の例で、"非同期" プログラミングの基本的な概念を確認できます。
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 の結果が適切な型に変換されます。
もう 1 つの重要な行は、Async.RunSynchronously
の呼び出しです。 これは、実際に F# の非同期計算を実行する場合に呼び出す必要がある非同期モジュール開始関数の 1 つです。
これは、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>
計算のシーケンスに変換します。printTotalFileBytes
計算を並列にスケジュールおよび実行する Async<'T[]>
を作成します。Async<unit>
を作成し、その結果 (unit[]
) を無視します。Async.RunSynchronously
で明示的に実行し、完了するまでブロックします。このプログラムが実行されると、printTotalFileBytes
がコマンド ライン引数ごとに並列で実行されます。 非同期計算はプログラム フローとは無関係に実行されるため、情報を出力して実行を終了する順序は定義されていません。 計算は並列でスケジュールされますが、その実行順序は保証されません。
Async<'T>
は、既に実行されているタスクではなく作業の仕様であるため、より複雑な変換を簡単に実行できます。 一連の非同期計算をシーケンス処理して、1 つずつ実行する例を次に示します。
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
これにより、並列でスケジュールする代わりに argv
の要素の順序で実行するように printTotalFileBytes
がスケジュールされます。 後続の各操作は、前の計算の実行が完了するまではスケジュールされないため、実行中に重複が発生しないように計算がシーケンス処理されます。
F# で非同期コードを記述する場合は、通常、計算のスケジューリングを処理するフレームワークと対話します。 ただし、常にそうであるとは限らないため、非同期処理のスケジュール設定に使用できるさまざまな関数について理解しておくことをお勧めします。
F# の非同期計算は、既に実行されている作業の表現ではなく、作業の "仕様" であるため、開始関数を使用して明示的に開始する必要があります。 さまざまなコンテキストで役に立つ非同期開始メソッドが多数あります。 次のセクションでは、一般的な開始関数の一部について説明します。
非同期計算内で子計算を開始します。 これにより、複数の非同期計算を同時に実行できます。 子計算では、キャンセル トークンが親計算と共有されます。 親計算が取り消された場合は、子計算も取り消されます。
署名:
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
が返されます。 多くの計算で構成される非同期計算で使用され、その計算の 1 つによって例外がスローされる場合、外側の計算は完全に停止されます。
署名:
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 ライブラリ、または async/await スタイルの非同期プログラミングを使用する C# コードベースとの相互運用が必要になることがあります。 C# と、大部分の .NET ライブラリでは、Task<TResult> と Task の型がコアの抽象化として使われるため、これにより F# の非同期コードの記述方法が変わる場合があります。
選択肢の 1 つは、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 を使用する API (つまり、値を返さない .NET 非同期計算) を操作するには、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()
の呼び出しは、タスクが完了するまで待機します。
この記事全体でスレッド処理について触れていますが、覚えておくべき 2 つの重要な点があります。
たとえば、作業の性質に応じて、実際には呼び出し元のスレッドで計算が実行される場合があります。 また、計算がスレッド間を "ジャンプ" して、それらを短時間借用し、"待機中" (ネットワーク呼び出しが転送中の場合など) の期間の合間に有用な作業を行うことができます。
F# を使用すると、現在のスレッドで (または明示的に現在のスレッド以外で) 非同期計算を開始できますが、非同期性は通常、特定のスレッド戦略に関連付けられていません。
.NET に関するフィードバック
.NET はオープンソース プロジェクトです。 フィードバックを提供するにはリンクを選択します。
トレーニング
ラーニング パス
F# は、オープンソースのクロスプラットフォーム プログラミング言語であり、簡潔でパフォーマンスが高く、堅牢で実用的なコードを簡単に記述できます。 これは、Web API、デスクトップ、IoT、ゲームなどの、数多くのさまざまな種類のアプリケーションを作成できる汎用言語です。