kaefer
kaefer

Reputation: 5751

Build a cancellable non-blocking background worker

Let's say I have a long-running calculation that I want to run in the background as not to block the UI thread. So I am going wrap it in an asynchronous computation.

/// Some long-running calculation...
let calculation arg = async{
    do! Async.Sleep 1000
    return arg }

Next I need to run the calculation in a loop where I switch to another thread to execute it and back to the UI thread to do something with its results.

/// Execute calculation repeatedly in for-loop and
/// display results on UI thread after every step
open System.Threading
let backgroundLoop uiAction = async {
    let ctx = SynchronizationContext.Current
    for arg in 0..100 do
        do! Async.SwitchToThreadPool()
        let! result = calculation arg
        do! Async.SwitchToContext ctx
        uiAction result }

Then this loop must be wrapped in another asynchronous computation to provide some means to cancel it from the UI.

/// Event-controlled cancellation wrapper
let cancelEvent = new Event<_>()
let cancellableWorker work = async {
    use cToken = new CancellationTokenSource()
    Async.StartImmediate(work, cToken.Token)
    do! Async.AwaitEvent cancelEvent.Publish
    cToken.Cancel() }

It seems now that I have implemented a functionality similar to that of BackgroundWorker. Testing it:

// Demo where the results are added to a ListBox.
// The background calculations can be stopped
// by a keypress when the ListBox has focus
open System.Windows.Forms
let fm = new Form()
let lb = new ListBox(Dock = DockStyle.Fill)
fm.Controls.Add lb
fm.Load.Add <| fun _ ->
    backgroundLoop (lb.Items.Add >> ignore)
    |> cancellableWorker 
    |> Async.StartImmediate
lb.KeyDown.Add <| fun _ ->
    cancelEvent.Trigger()

[<System.STAThread>]
#if INTERACTIVE
fm.Show()
#else
Application.Run fm
#endif

It appears to be a bit of effort for something which I imagine as a relatively common workflow. Can we simplify it? Am I missing anything crucial?

Upvotes: 2

Views: 297

Answers (1)

Taylor Wood
Taylor Wood

Reputation: 16194

Then this loop must be wrapped in another asynchronous computation to provide some means to cancel it from the UI.

Can we simplify it?

I think cancelEvent and cancellableWorker are unnecessary indirection in this case. You can use a CancellationTokenSource and cancel it directly from a UI event, instead of an Event<> that in turn cancels a token.

let calculation arg = async {
    do! Async.Sleep 1000
    return arg }

open System.Threading
let backgroundLoop uiAction = async {
    let ctx = SynchronizationContext.Current
    for arg in 0..100 do
        do! Async.SwitchToThreadPool()
        let! result = calculation arg
        do! Async.SwitchToContext ctx
        uiAction result }

open System.Windows.Forms
let fm = new Form()
let lb = new ListBox(Dock = DockStyle.Fill)
fm.Controls.Add lb

let cToken = new CancellationTokenSource()
fm.Load.Add <| fun _ ->
    Async.StartImmediate (backgroundLoop (lb.Items.Add >> ignore), cToken.Token)
lb.KeyDown.Add <| fun _ -> cToken.Cancel()

Also (if you haven't already) take a look at Tomas Petricek's article on non-blocking user-interfaces in F#.

Upvotes: 4

Related Questions