如何从按钮单击事件中取消与其他人并行运行的单个异步计算

时间:2011-05-01 23:57:42

标签: winforms asynchronous f#

我准备了以下WinForms代码尽可能简单,以帮助回答我的问题。你可以看到我有一个开始按钮,它可以并行设置并运行3个不同的异步计算,每个计算都可以完成一些工作,然后用结果更新标签。我有3个取消按钮对应于并行运行的每个异步计算。如何连接这些取消按钮以取消相应的异步计算,同时允许其他按钮继续并行运行?谢谢!

open System.Windows.Forms

type MyForm() as this =
    inherit Form()
    let lbl1 = new Label(AutoSize=true, Text="Press Start")
    let lbl2 = new Label(AutoSize=true, Text="Press Start")
    let lbl3 = new Label(AutoSize=true, Text="Press Start")

    let cancelBtn1 = new Button(AutoSize=true,Enabled=false, Text="Cancel")
    let cancelBtn2 = new Button(AutoSize=true,Enabled=false, Text="Cancel")
    let cancelBtn3 = new Button(AutoSize=true,Enabled=false, Text="Cancel")

    let startBtn = new Button(AutoSize=true,Text="Start")

    let panel = new FlowLayoutPanel(AutoSize=true, Dock=DockStyle.Fill, FlowDirection=FlowDirection.TopDown)
    do
        panel.Controls.AddRange [|startBtn; lbl1; cancelBtn1; lbl2; cancelBtn2; lbl3; cancelBtn3; |]
        this.Controls.Add(panel)

        startBtn.Click.Add <| fun _ ->
            startBtn.Enabled <- false
            [lbl1;lbl2;lbl3] |> List.iter (fun lbl -> lbl.Text <- "Loading...")
            [cancelBtn1;cancelBtn2;cancelBtn3] |> List.iter (fun cancelBtn -> cancelBtn.Enabled <- true)

            let guiContext = System.Threading.SynchronizationContext.Current

            let work (timeout:int) = //work is not aware it is being run within an async computation
                System.Threading.Thread.Sleep(timeout)
                System.DateTime.Now.Ticks |> string

            let asyncUpdate (lbl:Label) (cancelBtn:Button) timeout =
                async {
                    let result = work timeout //"cancelling" means forcibly aborting, since work may be stuck in an infinite loop
                    do! Async.SwitchToContext guiContext
                    cancelBtn.Enabled <- false
                    lbl.Text <- result
                }

            let parallelAsyncUpdates =
                [|asyncUpdate lbl1 cancelBtn1 3000; asyncUpdate lbl2 cancelBtn2 6000; asyncUpdate lbl3 cancelBtn3 9000;|]
                |> Async.Parallel
                |> Async.Ignore

            Async.StartWithContinuations(
                parallelAsyncUpdates,
                (fun _ -> startBtn.Enabled <- true),
                (fun _ -> ()),
                (fun _ -> ()))

2 个答案:

答案 0 :(得分:3)

非合作取消线程通常是一种不好的做法,所以我不建议这样做。请参阅示例this article。当您使用Thread直接编程时(使用Thread.Abort)可以完成此操作,但.NET的现代并行/异步库(例如TPL或F#Async)都不会使用它。如果那真的是你需要的,那么你必须明确地使用线程。

更好的选择是更改work功能,以便可以合作取消它。在F#中,这实际上只是意味着将其包含在async内并使用let!do!,因为这会自动插入取消支持。例如:

let work (timeout:int) = async {
  do! Async.Sleep(timeout)
  return System.DateTime.Now.Ticks |> string }

不使用async(例如,如果函数是用C#编写的),您可以传递CancellationToken值并使用它来检查是否请求取消(通过调用ThrowIfCancellationRequestsd) 。然后,您可以使用Async.Start开始三次计算(为每个计算创建一个新的CancellationTokenSource)。

要做完所有事情,直到它们全部完成,我可能会创建一个简单的代理(触发一些事件,直到它收到指定数量的消息)。我认为还没有更直接的方法(因为Async.Parallel对所有工作流使用相同的取消令牌)。

所以,我想这个答案的重点是 - 如果要取消work,那么它应该知道这种情况,以便它可以适当地处理它。

答案 1 :(得分:1)

正如Tomas所提到的,强行停止一个线程是一个坏主意,设计一些没有意识到它的东西是一个能够阻止引发标志的线程,在我看来,但是,如果你做了很长的计算,如果您正在使用某些数据结构,例如2或3D数组,那么一个选项就是能够将其设置为null,但这违反了F#的许多概念,因为您的函数正在处理应该不仅是不可变的,但如果有一些数组将要改变,那么没有别的东西可以改变它。

例如,如果你需要停止正在处理文件的线程(之前我必须这样做),那么,由于文件无法删除,因为它已打开,然后我就能打开它了在记事本中,然后只删除所有内容,并保存,线程崩溃。

所以,你可能想要做这样的事情来实现你的目标,但是,我建议你重新评估你的设计,看看是否有更好的方法来做到这一点。