2012-03-07 80 views
2

我尝试创建基于用户交互更新UI的代理。如果用户点击一个按钮,应该刷新GUI。模型的准备需要很长时间,所以如果用户点击其他按钮,则希望取消准备并开始新的准备。Async.TryCancelled无法与Async.Run同步使用

我有什么至今:

open System.Threading 
type private RefreshMsg = 
    | RefreshMsg of AsyncReplyChannel<CancellationTokenSource> 

type RefresherAgent() = 
    let mutable cancel : CancellationTokenSource = null 

    let doSomeModelComputation i = 
     async { 
      printfn "start %A" i 
      do! Async.Sleep(1000) 
      printfn "middle %A" i 
      do! Async.Sleep(1000) 
      printfn "end %A" i 
     } 
    let mbox = 
     MailboxProcessor.Start(fun mbx -> 
      let rec loop() = async { 
       let! msg = mbx.Receive() 
       match msg with 
       | RefreshMsg(chnl) -> 
        let cancelSrc = new CancellationTokenSource() 
        chnl.Reply(cancelSrc) 
        let update = async { 
            do! doSomeModelComputation 1 
            do! doSomeModelComputation 2 
            //do! updateUI // not important now 
           } 
        let cupdate = Async.TryCancelled(update, (fun c -> printfn "refresh cancelled")) 
        Async.RunSynchronously(cupdate, -1, cancelSrc.Token) 
        printfn "loop()" 
        return! loop() 
      } 
      loop()) 
    do 
     mbox.Error.Add(fun exn -> printfn "Error in refresher: %A" exn) 
    member x.Refresh() = 
     if cancel <> null then 
      // I don't handle whether the previous computation finished 
      // I just cancel it; might be improved 
      cancel.Cancel() 
      cancel.Dispose() 
     cancel <- mbox.PostAndReply(fun reply -> RefreshMsg(reply)) 
     printfn "x.Refresh end" 

//sample 
let agent = RefresherAgent() 
agent.Refresh() 
System.Threading.Thread.Sleep(1500) 
agent.Refresh() 

我返回CancellationTokenSource为每个请求并将其存储在一个可变的变量(x.Refresh()是线程安全的,它被称为在UI线程)。 如果第一次调用Refresh(),则返回取消源。如果第二次调用Refresh(),我调用Cancel应该中止通过Async.RunSynchronously运行的异步任务。

但是,引发了一个例外。从我的样本输出是

x.Refresh end 
start 1 
middle 1 
end 1 
refresh cancelled 
Error in refresher: System.OperationCanceledException: The operation was canceled. 
    at Microsoft.FSharp.Control.AsyncBuilderImpl.commit[a](Result`1 res) 

现在,因为我认为这一点,它可能是有意义的,因为在其上运行代理线程,被interrputed,对不对?但是,我如何达到理想的行为?


我需要取消代理内部工作流程异步,从而使代理可以继续消费新邮件。为什么使用邮箱处理器?因为它保证只有一个线程正在尝试创建UI模型,所以我节省了资源。

让我们假设我通过从几个Web服务下载数据来创建UI模型,这就是为什么我使用异步调用。当用户更改组合并选择其他选项时,我想停止使用旧值查询webservices(=取消异步调用),并希望以新值创建新的模型基础Web服务调用。

任何建议,我可以使用,而不是我的解决方案,并将解决我的问题,也是受欢迎的。

回答

2

我在试图理解你想达到什么时遇到困难。但也许这并不重要 - 错误只是说您使用RunSynchronously执行的工作流被取消(RunSynchronously将抛出异常) - 因此您可以将此调用包装为try-match块并忽略OC-Exception

一个更好的选择可能是重构你的cupdate和这里面的尝试匹配 - 你甚至可以把在TryCancelled这个问题,如果你直接赶OC-例外;)

let update = 
    async { 
     try 
     do! doSomeModelComputation 1 
     do! doSomeModelComputation 2 
     with 
     | :? OperationCanceledException -> 
      printfn "refresh cancelled" 
    } 
Async.RunSynchronously(update, -1, cancelSrc.Token) 

但我仍然没有得到你想要的这部分同步

+0

刚刚添加了一段来更好地解释背景。 – stej 2012-03-07 16:29:08

+0

你说得对,这是一个解决方案,但我希望有一个更好的解决方案。也许是来自'Async'的其他方法的组合(除了'TryCancelled'和'RunSynchronously')。 – stej 2012-03-07 16:30:59

+0

我已经添加了一个建议,我的答案...我不能测试这个,但我*认为*它应该按预期工作 – Carsten 2012-03-07 16:35:44