次の方法で共有


タスクの表現

この記事では、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 ..の制御フローコンストラクトを含めることができます。 これらには、同期的に実行される with ハンドラーと finally ハンドラーを除き、さらにタスクコンストラクトが含まれる場合があります。 非同期try .. finally ..が必要な場合は、use型のオブジェクトと組み合わせてIAsyncDisposable バインディングを使用します。

use バインドと use! バインド

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

let!に加えて、use!を使用して非同期バインディングを実行できます。 let!use!の違いは、letuseの違いと同じです。 use!の場合、オブジェクトは現在のスコープの終了時に破棄されます。 F# 6 では、use! では値を null に初期化することはできませんが、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 には複数のオーバーロードがあり、そのうちの 1 つはキャンセル トークンを受け入れます。 このオーバーロードを使用しない場合、その特定の非同期読み取り操作はキャンセルできません。

バックグラウンド タスク

既定では、.NET タスクは、 SynchronizationContext.Current (存在する場合) を使用してスケジュールされます。 これにより、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# コンパイラによって "再開可能なステート マシン" にコンパイルされます。 これらは、 再開可能なコード RFCF# コンパイラ コミュニティ セッションで詳しく説明されています。

こちらも参照ください