Reputation: 5751
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
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