2

ユーザーの操作に基づいて 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リクエストごとに a を返し、それを可変変数に格納します (x.Refresh() はスレッド セーフであり、UI スレッドで呼び出されます)。初めて Refresh() を呼び出すと、キャンセル元が返されます。Refresh() が 2 回目に呼び出された場合は、Async.RunSynchronously で実行する非同期タスクを中止する Cancel を呼び出します。

ただし、例外が発生します。私のサンプルからの出力は

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)

これについて考えると、エージェントが実行されているスレッドが中断されたので、それは理にかなっているかもしれませんよね? しかし、どうすれば望ましい動作を実現できますか?


エージェントが新しいメッセージを引き続き使用できるように、エージェント内の非同期ワークフローをキャンセルする必要があります。なぜメールボックス プロセッサを使用するのですか? 1 つのスレッドだけが UI モデルを作成しようとしていることが保証されているため、リソースを節約できます。

いくつかの Web サービスからデータをダウンロードして UI モデルを作成するとします。そのため、非同期呼び出しを使用します。ユーザーがコンボを変更して他のオプションを選択すると、古い値で Web サービスのクエリを停止 (= 非同期呼び出しをキャンセル) し、新しい値で Web サービス呼び出しの新しいモデル ベースを作成したいと考えています。

私のソリューションの代わりに使用でき、私の問題を解決する提案も大歓迎です。

4

1 に答える 1

2

あなたが達成したいことを理解しようとするのは難しいです。しかし、これは問題ではないかもしれません-エラーは、実行中のワークフローRunSynchronouslyがキャンセルされたことを示しているだけです(RunSynchronously は例外をスローします)-したがって、この呼び出しを try-match ブロックにラップし、OC-Exception を無視することができます

より良いオプションは、cupdate をリファクタリングして、この内部の try-match にすることです。OC-Exceptions を直接キャッチする場合は、TryCancelled をその中に持ち込むこともできます;)

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

しかし、私はまだあなたがこれを必要とする理由の一部を理解していません。

于 2012-03-07T16:09:46.337 に答える