この記事では、F# でのタスク式のサポートについて説明します。これは 非同期式 に似ていますが、.NET タスクを直接作成できます。 非同期式と同様に、タスク式はコードを非同期的に実行します。つまり、他の作業の実行をブロックすることはありません。
非同期コードは通常、非同期式を使用して作成されます。 .NET タスクを作成または使用する .NET ライブラリと広範囲に相互運用する場合は、タスク式を使用することをお勧めします。 タスク式を使用すると、パフォーマンスとデバッグ エクスペリエンスを向上させることもできます。 ただし、タスク式にはいくつかの制限があり、この記事の後半で説明します。
構文
task { expression }
前の構文では、 expression
によって表される計算が .NET タスクとして実行されるように設定されています。 このコードが実行された直後にタスクが開始され、最初の非同期操作 (非同期スリープ、非同期 I/O、その他のプリミティブ非同期操作など) が実行されるまで、現在のスレッドで実行されます。 式の型は Task<'T>
です。ここで、 'T
は、 return
キーワードが使用されるときに式によって返される型です。
let を使用したバインド。
タスク式では、一部の式と操作は同期であり、一部は非同期です。 通常の let
バインディングではなく、非同期操作の結果を待機するときは、 let!
を使用します。
let!
の効果は、計算の実行中に他の計算またはスレッドで実行を続行できるようにすることです。
let!
バインドの右側が戻った後、タスクの残りの部分は実行を再開します。
次のコードは、 let
と let!
の違いを示しています。
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 { }
式は、次の種類の非同期操作を待機できます。
- .NET タスク、Task<TResult>、非ジェネリック Task。
- .NET 値タスク ValueTask<TResult> および非ジェネリック ValueTask。
- F# 非同期処理
Async<T>
。 - F# RFC FS-1097 で指定された "GetAwaiter" パターンに続く任意のオブジェクト。
return
式
タスク式内では、 return expr
を使用してタスクの結果を返します。
return!
式
タスク式内では、 return! expr
を使用して別のタスクの結果を返します。 これは、 let!
を使用してすぐに結果を返すのと同じです。
制御フロー
タスク式には、 for .. in .. do
、 while .. do
、 try .. with ..
、 try .. finally ..
、 if .. then .. else
、 if .. then ..
の制御フローコンストラクトを含めることができます。 これらには、同期的に実行される with
ハンドラーと finally
ハンドラーを除き、さらにタスクコンストラクトが含まれる場合があります。 非同期try .. finally ..
が必要な場合は、use
型のオブジェクトと組み合わせてIAsyncDisposable
バインディングを使用します。
use
バインドと use!
バインド
タスク式内では、 use
バインドは、 IDisposable 型または IAsyncDisposable型の値にバインドできます。 後者の場合、破棄クリーンアップ操作は非同期的に実行されます。
let!
に加えて、use!
を使用して非同期バインディングを実行できます。
let!
とuse!
の違いは、let
とuse
の違いと同じです。
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# コンパイラによって "再開可能なステート マシン" にコンパイルされます。 これらは、 再開可能なコード RFC と F# コンパイラ コミュニティ セッションで詳しく説明されています。
こちらも参照ください
.NET