タスク式

この記事では、タスク式の 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 .. が必要な場合は、 型 IAsyncDisposable のオブジェクトと組み合わせて use のバインドを使用します。

useuse! のバインド

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

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 には複数のオーバーロードがあります。そのうちの 1 つがキャンセル トークンを受け入れます。 このオーバーロードを使用しない場合、その特定の非同期読み取り操作はキャンセル可能ではありません。

バックグラウンド タスク

既定では、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# 非同期式とは異なり、タスク式では tailcalls はサポートされていません。 つまり、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 が必要な場合は、tailcalls をサポートする 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# コンパイラ コミュニティ セッションで詳しく説明されています。

関連項目