オブジェクトのCancel
メンバーはCancellationTokenSource
「キャンセルのリクエストを伝達する」と思いますが、これは、それが発火して忘れられ、キャンセルが完了するまで待機しないことを意味します (たとえば、すべての例外ハンドラーが実行されます)。それはいいことですが、別の非同期を作成する前に、未処理の非同期が完全にキャンセルされるまで待つ必要があります。これを達成する簡単な方法はありますか?
2 に答える
使いやすい同期プリミティブを考えると、これは難しいことではありません。私は特に、一度だけ書き込める「論理」変数が好きです。
type Logic<'T> =
new : unit -> Logic<'T>
member Set : 'T -> unit
member Await : Async<'T>
Async をラップして、完了時に論理変数を設定し、それを待機するのは簡単です。次に例を示します。
type IWork =
abstract member Cancel : unit -> Async<unit>
let startWork (work: Async<unit>) =
let v = Logic<unit>()
let s = new CancellationTokenSource()
let main = async.TryFinally(work, fun () -> s.Dispose(); v.Set())
Async.Start(main, s.Token)
{
new IWork with
member this.Cancel() = s.Cancel(); v.Await
}
論理変数の可能な実装は次のとおりです。
type LogicState<'T> =
| New
| Value of 'T
| Waiting of ('T -> unit)
[<Sealed>]
type Logic<'T>() =
let lockRoot = obj ()
let mutable st = New
let update up =
let k =
lock lockRoot <| fun () ->
let (n, k) = up st
st <- n
k
k ()
let wait (k: 'T -> unit) =
update <| function
| New -> (Waiting k, ignore)
| Value value as st -> (st, fun () -> k value)
| Waiting f -> (Waiting (fun x -> f x; k x), ignore)
let await =
Async.FromContinuations(fun (ok, _, _) -> wait ok)
member this.Set<'T>(value: 'T) =
update <| function
| New -> (Value value, ignore)
| Value _ as st -> (st, ignore)
| Waiting f as st -> (Value value, fun () -> f value)
member this.Await = await
F# 非同期ライブラリの標準ライブラリ関数を使用してそれを行う直接的な方法はないと思います。Async.TryCancelled
ワークフローが (実際に) キャンセルされたときにコールバックを実行する最も近い操作ですが、ワークフローを開始したコードにコールバックからメッセージを送信するには、手動で行う必要があります。
これは、イベントと、私が作成した F# 非同期拡張機能 (FSharpX パッケージにも含まれています) の拡張機能を使用して比較的簡単に解決GuardedAwaitObservable
できます。手術)。
次のAsync.StartCancellable
メソッドは、非同期ワークフローを受け取り、 を返しますAsync<Async<unit>>
。外側のワークフローにバインドすると、( のような) 引数が開始Async.StartChild
され、返された内側のワークフローにバインドすると、計算がキャンセルされ、実際にキャンセルされるまで待機します。
open System.Threading
module Async =
/// Returns an asynchronous workflow 'Async<Async<unit>>'. When called
/// using 'let!', it starts the workflow provided as an argument and returns
/// a token that can be used to cancel the started work - this is an
/// (asynchronously) blocking operation that waits until the workflow
/// is actually cancelled
let StartCancellable work = async {
let cts = new CancellationTokenSource()
// Creates an event used for notification
let evt = new Event<_>()
// Wrap the workflow with TryCancelled and notify when cancelled
Async.Start(Async.TryCancelled(work, ignore >> evt.Trigger), cts.Token)
// Return a workflow that waits for 'evt' and triggers 'Cancel'
// after it attaches the event handler (to avoid missing event occurrence)
let waitForCancel = Async.GuardedAwaitObservable evt.Publish cts.Cancel
return async.TryFinally(waitForCancel, cts.Dispose) }
EDIT Jonが提案しTryFinally
たように、結果を破棄して破棄します。CancellationTokenSource
これは、正しく廃棄されることを確認するのに十分なはずだと思います。
メソッドを使用した例を次に示します。このloop
関数は、テストに使用した単純なワークフローです。コードの残りの部分は、それを開始し、5.5 秒待ってからキャンセルします。
/// Sample workflow that repeatedly starts and stops long running operation
let loop = async {
for i in 0 .. 9999 do
printfn "Starting: %d" i
do! Async.Sleep(1000)
printfn "Done: %d" i }
// Start the 'loop' workflow, wait for 5.5 seconds and then
// cancel it and wait until it finishes current operation
async { let! cancelToken = Async.StartCancellable(loop)
printfn "started"
do! Async.Sleep(5500)
printfn "cancelling"
do! cancelToken
printfn "done" }
|> Async.Start
完全を期すために、FSharpX からの必要な定義を含むサンプルは、F# スニペットにあります。