我尝试创建一个基于用户交互更新 UI 的代理。如果用户单击按钮,则应刷新 GUI。模型的准备需要很长时间,所以如果用户点击其他按钮,则取消准备并开始新的准备是可取的。
到目前为止我所拥有的:
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)
现在当我想到这一点时,这可能是有道理的,因为运行代理的线程被中断了,对吧?但是,我如何实现所需的行为?
我需要取消代理内部的异步工作流,以便代理可以继续使用新消息。为什么要使用邮箱处理器?因为保证只有一个线程在尝试创建 UI 模型,所以我节省了资源。
假设我通过从多个 Web 服务下载数据来创建 UI 模型,这就是我使用异步调用的原因。当用户更改组合并选择其他选项时,我想停止使用旧值查询 Web 服务(= 取消异步调用),并希望创建具有新值的新模型基础 od Web 服务调用。
任何我可以使用而不是我的解决方案并能解决我的问题的建议也是受欢迎的。