构建可取消的非阻塞后台工作程序

时间:2018-02-10 10:48:46

标签: winforms f# f#-async

假设我有一个长时间运行的计算,我想在后台运行,因为不阻止UI线程。所以我将它包装在一个异步计算中。

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

接下来我需要在一个循环中运行计算,在这个循环中我切换到另一个线程来执行它并返回到UI线程以对其结果做一些事情。

/// 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 }

然后,必须将此循环包装在另一个异步计算中,以提供一些从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() }

现在看来,我已经实现了类似于BackgroundWorker的功能。测试它:

// 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

对于我认为相对常见的工作流程来说,似乎需要付出一些努力。我们可以简化它吗?我错过了什么至关重要的事情吗?

1 个答案:

答案 0 :(得分:4)

  

然后,必须将此循环包装在另一个异步计算中,以提供一些从UI中取消它的方法。

     

我们可以简化吗?

我认为cancelEventcancellableWorker在这种情况下是不必要的间接。您可以使用CancellationTokenSource直接从UI事件取消它,而不是取消令牌的Event<>

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()

另外(如果你还没有)看看Tomas Petricek's article on non-blocking user-interfaces in F#