非同期式

この記事では、非同期式での F# のサポートについて説明します。 非同期式は、計算を非同期的に実行する、つまり、他の作業の実行をブロックしない方法の 1 つを提供します。 たとえば、非同期計算を使用すると、ユーザーに対する UI の応答性を維持しつつ他の作業を実行できるアプリケーションを作成できます。 F# 非同期ワークフロー プログラミング モデルを使うと、スレッド遷移の詳細をライブラリ内に隠しながら関数型プログラムを作成できます。

非同期コードは、.NET タスクを直接作成するタスク式を使用して作成することもできます。 .NET タスクを作成または使用する .NET ライブラリと広範囲で相互運用する場合は、タスク式の使用が推奨されます。 F# でほとんどの非同期コードを記述する場合は、より簡潔で構成性が高く、.NET タスクに関連する特定の注意が回避される F# 非同期式が推奨されます。

構文

async { expression }

解説

前の構文では、expression によって表される計算が非同期的に実行されるように設定されています。つまり、非同期スリープ操作、I/O、その他の非同期操作が実行されるときに、現在の計算スレッドはブロックされません。 多くの場合、非同期計算はバックグラウンド スレッドで開始され、現在のスレッドの実行は継続します。 式の型は Async<'T> です。ここで、'T は、return キーワードが使用されている場合に式によって返される型です。

Async クラスには、いくつかのシナリオをサポートするメソッドが含まれています。 一般的な方法は、非同期的に実行しようとしている 1 つまたは複数の計算を表す Async オブジェクトを作成してから、トリガー関数のいずれか 1 つを使用してこれらの計算を開始するというものです。 使用するトリガーは、現在のスレッド、バックグラウンド スレッド、または .NET タスク オブジェクトのどれを使用するかによって異なります。 たとえば、現在のスレッドで非同期計算を開始するには、Async.StartImmediate を使用できます。 UI スレッドから非同期計算を開始する場合は、キーボード操作やマウス アクティビティなどのユーザー アクションを処理するメイン イベント ループがブロックされないため、アプリケーションの応答性が維持されます。

let! を使用した非同期バインド

非同期式では、一部の式と操作は同期的であり、一部は非同期です。 メソッドを非同期的に呼び出す場合は、通常の let バインドではなく、let! を使用します。 let! の効果は、計算の実行中に他の計算やスレッドで実行を続行できるようになるというものです。 let! バインドの右側が返されたら、非同期式の残りの部分では実行が再開されます。

次のコードは、letlet! の違いを示しています。 let を使用するコード行では、非同期計算が、Async.StartImmediateAsync.RunSynchronously などを使用して後で実行できるオブジェクトとして作成されるに過ぎません。 let! を使用するコード行では、計算が開始され、非同期の待機が実行されます。結果が使用可能になるまでスレッドは中断し、その後、実行が続行されます。

// let just stores the result as an asynchronous operation.
let (result1 : Async<byte[]>) = stream.AsyncRead(bufferSize)
// let! completes the asynchronous operation and returns the data.
let! (result2 : byte[])  = stream.AsyncRead(bufferSize)

let! は、F# 非同期計算 Async<T> を直接待機する場合にのみ使用できます。 他の種類の非同期操作を間接的に待機することができます。

  • Async.AwaitTask と組み合わせることで、.NET タスク Task<TResult> と非ジェネリックの Task
  • .AsTask() および Async.AwaitTask と組み合わせることで、.NET 値タスク ValueTask<TResult> と非ジェネリックの ValueTask
  • task { return! expr } |> Async.AwaitTask と組み合わせることで、F# RFC FS-1097 で指定された "GetAwaiter" パターンに続くオブジェクト。

制御フロー

非同期式には、for .. in .. dowhile .. dotry .. with ..try .. finally ..if .. then .. elseif .. then .. などの制御フロー コンストラクトを含めることができます。 さらに、同期的に実行される withfinally のハンドラーを除き、追加で非同期のコンストラクトが含まれる場合があります。

F# 非同期式では、非同期の try .. finally .. はサポートされていません。 この場合は、タスク式を使用できます。

useuse! のバインド

非同期式内では、use バインドを IDisposable 型の値にバインドできます。 後者の場合、廃棄クリーンアップ操作は非同期的に実行されます。

let! に加えて、use! を使用して非同期バインドを実行することもできます。 let!use! の違いは、letuse の違いと同じです。 use! の場合、オブジェクトは現在のスコープの終了時に破棄されます。 現在のリリースの F# では、use! で値を null に初期化することはできないことにご注意ください。一方、use ではそれが可能です。

非同期プリミティブ

1 つの非同期タスクを実行し、その結果を返すメソッドは、"非同期プリミティブ" と呼ばれており、これは let! 専用に設計されています。 いくつかの非同期プリミティブは、F# コア ライブラリで定義されています。 Web アプリケーション用にそのようなメソッドが 2 つ、モジュール FSharp.Control.WebExtensions に定義されています。WebRequest.AsyncGetResponseWebClient.AsyncDownloadString です。 どちらのプリミティブも、URL を指定されると、Web ページからデータをダウンロードします。 AsyncGetResponseSystem.Net.WebResponse オブジェクトを生成し、AsyncDownloadString は Web ページの HTML を表す文字列を生成します。

非同期 I/O 操作のためのいくつかのプリミティブが FSharp.Control.CommonExtensions モジュールに含まれています。 System.IO.Stream クラスの拡張メソッドは Stream.AsyncReadStream.AsyncWrite です。

また、本体が非同期式である関数またはメソッドを定義することで、独自の非同期プリミティブを記述できます。

F# の非同期プログラミング モデルを使用して他の非同期モデル用に設計された非同期メソッドを .NET Framework で使用するには、F# Async オブジェクトを返す関数を作成します。 F# ライブラリには、これを簡単に実行できる関数が用意されています。

非同期式の使用例を次に示します。Async クラスのメソッドに関するドキュメントには、他にも多数の例が含まれています。

この例では、非同期式を使用してコードを並列実行する方法を示します。

次のコード例では、関数 fetchAsync は Web 要求から返された HTML テキストを取得します。 fetchAsync 関数には、非同期のコード ブロックが含まれています。 非同期プリミティブ (このケースでは AsyncDownloadString) の結果に対してバインドを行う場合、let の代わりに let! が使用されます。

関数 Async.RunSynchronously を使用して非同期操作を実行し、その結果を待機します。 たとえば、Async.Parallel 関数を Async.RunSynchronously 関数と共に使用することで、複数の非同期操作を並列で実行できます。 Async.Parallel 関数は、Async オブジェクトの一覧を受け取り、Async タスク オブジェクトごとにコードを並列で実行するように設定し、並列計算を表す Async オブジェクトを返します。 1 つの操作の場合と同じように、Async.RunSynchronously を呼び出して実行を開始します。

runAll 関数は、3 つの非同期式を並列で起動し、すべてが完了するまで待機します。

open System.Net
open Microsoft.FSharp.Control.WebExtensions

let urlList = [ "Microsoft.com", "http://www.microsoft.com/"
                "MSDN", "http://msdn.microsoft.com/"
                "Bing", "http://www.bing.com"
              ]

let fetchAsync(name, url:string) =
    async {
        try
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)
            printfn "Read %d characters for %s" html.Length name
        with
            | ex -> printfn "%s" (ex.Message);
    }

let runAll() =
    urlList
    |> Seq.map fetchAsync
    |> Async.Parallel
    |> Async.RunSynchronously
    |> ignore

runAll()

関連項目