4

I'm trying to make an async workflow, where there's a main async loop, which executes an async sub-block in each loop. And I want this async sub-block to be cancellable, but when it cancels then I don't want the main loop to cancel. I want it to continue, at the line after the do! subBlock.

The only method I see in Async that even has an acceptable signature (takes CancellationToken, returns something that can be converted to async) is Async.StartAsTask, but that seems to hang when canceled; in the below, it prints "cancelled" 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 any way to do this?

svick
  • 236,525
  • 50
  • 385
  • 514
Dax Fohl
  • 10,654
  • 6
  • 46
  • 90

2 Answers2

2

Whether the caller that starts and cancels the worker is an async too doesn't really affect this problem, since the worker is managed via its explicitly specified cancellation token.

Asyncs have three continutations: the normal one, which can return a value, one for exceptions, and one for cancellation. There are multiple ways to add a cancellation continuation to an async, such as Async.OnCancel, Async.TryCancelled, or the general Async.FromContinuations, which includes the 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 runMain is an async. In this simple case, you could also just have it print the "cancelled" message itself.

I hope this helps. I don't think there is a general answer to how to structure the program; that depends on the concrete use case.

Vandroiy
  • 6,163
  • 1
  • 19
  • 28
1

What happens here is that when your child task is cancelled, the OperationCanceledException brings down your mainBlock as well. I was able to get it to work by 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 cancelled, mainBlock is explicitly restarted in the cancelation handler. You need to add #nowarn "40" for it since mainBlock is used inside its definition. Also note the dispose on token source.

You can find more information on this problem (and perhaps a nicer solution in the form of StartCatchCancellation) in these two threads.

Community
  • 1
  • 1
scrwtp
  • 13,437
  • 2
  • 26
  • 30