Cancel workflow subblock a # a
I am trying to create an asynchronous workflow where there is a main async loop that executes an asynchronous sub-block in every loop. And I want this asynchronous sub-block to be canceled, but when it is canceled I don't want the main loop to be canceled. I want it to continue, on the line after do! subBlock
.
The only method I can see in Async
that even has an acceptable signature (accepts CancellationToken
, returns what can be converted to Async
) Async.StartAsTask
, but it looks like it hangs when canceled; at the bottom, it prints "canceled" and then nothing else.
open System
open System.Threading
open System.Threading.Tasks
// runs until cancelled
let subBlock =
async {
try
while true do
printfn "doing it"
do! Async.Sleep 1000
printfn "did it"
finally
printfn "cancelled!"
}
[<EntryPoint>]
let main argv =
let ctsRef = ref <| new CancellationTokenSource()
let mainBlock =
//calls subBlock in a loop
async {
while true do
ctsRef := new CancellationTokenSource()
do! Async.StartAsTask(subBlock, TaskCreationOptions.None, (!ctsRef).Token)
|> Async.AwaitTask
printfn "restarting"
}
Async.Start mainBlock
//loop to cancel CTS at each keypress
while true do
Console.ReadLine() |> ignore
(!ctsRef).Cancel()
0
Is there a way to do this?
source to share
What happens is that when your child task is canceled, it OperationCanceledException
also resets yours mainBlock
. I was able to get it to work using this:
let rec mainBlock =
async {
ctsRef := new CancellationTokenSource()
let task = Async.StartAsTask(subBlock, TaskCreationOptions.None, (!ctsRef).Token) |> Async.AwaitTask
do! Async.TryCancelled(task, fun e ->
(!ctsRef).Dispose()
printfn "restarting"
Async.Start mainBlock)
}
When the task is canceled, it is mainBlock
explicitly restarted in the cancellation handler. You need to add #nowarn "40"
for it as it is used in its definition mainBlock
. Also note that expose the source to the token.
More information on this issue (and perhaps a nicer form solution StartCatchCancellation
) can be found in these two threads .
source to share
Whether the caller who starts and cancels the job is asynchronous does not affect this issue either, since the worker is controlled through an explicitly specified cancellation token.
Asyncs have three permutations: normal, which can return a value, one for exceptions, and one for cancellation. There are several ways to add continue undo in async, for example Async.OnCancel
, Async.TryCancelled
or a general Async.FromContinuations
one that includes an exception case. Here's a program that has the desired output:
let rec doBlocks () =
async { printfn "doing it"
do! Async.Sleep 1000
printfn "did it"
do! doBlocks () }
let rec runMain () =
use cts = new CancellationTokenSource()
let worker = Async.TryCancelled(doBlocks (), fun _ -> printfn "Cancelled")
Async.Start(worker, cts.Token)
let k = Console.ReadKey(true)
cts.Cancel()
if k.Key <> ConsoleKey.Q then runMain ()
This works just as well if it runMain
is asynchronous. In this simple case, you can also just print the "canceled" message.
Hope this helps. I don't think there is a general answer to how to structure a program; it depends on the specific use case.
source to share