Search code examples
f#resumesuspendasync-workflow

Suspend and resume F# asynchronous workflows (Async<'T>)


Is there a general way to gain external control over the execution of an arbitrary asynchronous workflow, for suspension and resumption? Akin to a CancellationToken but more like a SuspendResumeToken where one could simply do things like:

let suspendResumeToken = new SuspendResumeToken()
Async.Start(longRunningAsync, suspendResumeToken=suspendResumeToken)
...
// meanwhile
async {
   do! suspendResumeToken.SuspendAsync()
   printfn "Suspended longRunningAsync!"
   do! suspendResumeToken.ResumeAsync()
   printfn "Resumed longRunningAsync!"
}

I could emulate the behavior for recursive or iterated functions, by checking such a token at every iteration or recursion step, but since async workflows have natural yield points, it would be natural to have that built-in, as a general way to control async scheduling.


Solution

  • Implementing something like your SuspendResumeToken is easily done using events. You can define an event, await it in the computation and then trigger it from the outside to resume the computation:

    let suspendEvent = new Event<_>()
    let awaitSuspendResume () = suspendEvent.Publish |> Async.AwaitEvent
    
    let longRunningAsync () = async {
       do! awaitSuspendResume ()
       printfn "Suspended longRunningAsync!"
       do! awaitSuspendResume ()
       printfn "Resumed longRunningAsync!"
    }
    
    Async.Start(longRunningAsync())
    
    suspendEvent.Trigger() // Run this multiple times to make a step
    

    There is no way to get this automatically at yield-points in async, but you could define your own computation builder which is exactly like async, except that it inserts the check in the Bind operation (and possibly some other places - you'd have to think about where exactly you want to do this):

    let suspendEvent = new Event<_>()
    let awaitSuspendResume () = suspendEvent.Publish |> Async.AwaitEvent
    
    type SuspendAsyncBuilder() = 
      // Custom 'Bind' with the extra check
      member x.Bind(a:Async<'T>, f:'T -> Async<'R>) : Async<'R> = 
        async { 
          let! av = a
          do! awaitSuspendResume()
          return! f av }
      // All other operations just call normal 'async'
      // (you will need to add many more here!)
      member x.Zero() = async.Zero()
    
    let sasync = SuspendAsyncBuilder()
    
    let longRunningAsync () = sasync {
       do! Async.Sleep(0)
       printfn "Suspended longRunningAsync!"
       do! Async.Sleep(0)
       printfn "Resumed longRunningAsync!"
    }
    
    Async.Start(longRunningAsync())
    suspendEvent.Trigger()
    

    Note that this only checks around ! operations like do! so I had to insert a Sleep in the example to make it work.