邮箱处理器和异常

18

我想知道为什么 MailboxProcessor 处理异常的默认策略是 默默忽略 它们。例如:

let counter =
    MailboxProcessor.Start(fun inbox ->
        let rec loop() =
            async { printfn "waiting for data..."
                    let! data = inbox.Receive()
                    failwith "fail" // simulate throwing of an exception
                    printfn "Got: %d" data
                    return! loop()
            }
        loop ())
()
counter.Post(42)
counter.Post(43)
counter.Post(44)
Async.Sleep 1000 |> Async.RunSynchronously

什么都没有发生。程序执行没有致命的停止,也没有弹出“未处理的异常”消息框。什么也没有。

如果有人使用PostAndReply方法,则情况会变得更糟:死锁是必然结果。

这种行为的原因是什么?

2个回答

16

2
是的,我知道这个成员Error。我想知道为什么默认的错误处理程序什么都不做。它可以重新抛出异常,或写入到stderr,或停止应用程序——任何事情都比默默忽略异常要好。 - qehgt
2
糟糕!我正在寻找标准的MailboxProcessor上的现有事件,但不知何故完全错过了Error事件... - Tomas Petricek
1
请注意,如果您希望程序在MailboxProcessor失败时崩溃而不是继续以可能破损的状态运行,您也可以raise异常而不是将其打印出来。 - spiffytech

6
我认为F#中的MailboxProcessor没有包含任何处理异常的机制,原因是不清楚最好的处理方式是什么。例如,您可能希望有一个全局事件,在发生未处理的异常时触发该事件,但您可能希望在下一次调用PostPostAndReply时重新抛出异常。
这两个选项都可以基于标准的MailboxProcessor实现,因此可以添加所需的行为。例如,以下代码片段显示了HandlingMailbox,它添加了全局异常处理程序。它具有与普通MailboxProcessor相同的接口(我省略了一些方法),但它添加了OnError事件,当发生异常时会触发该事件:
type HandlingMailbox<'T> private(f:HandlingMailbox<'T> -> Async<unit>) as self =
  let event = Event<_>()
  let inbox = new MailboxProcessor<_>(fun inbox -> async {
    try 
      return! f self
    with e ->
      event.Trigger(e) })
  member x.OnError = event.Publish
  member x.Start() = inbox.Start()
  member x.Receive() = inbox.Receive()
  member x.Post(v:'T) = inbox.Post(v)
  static member Start(f) =
    let mbox = new HandlingMailbox<_>(f)
    mbox.Start()
    mbox

要使用它,您将编写与之前相同的代码,但现在可以异步处理异常:

let counter = HandlingMailbox<_>.Start(fun inbox -> async {
  while true do 
    printfn "waiting for data..." 
    let! data = inbox.Receive() 
    failwith "fail" })

counter.OnError.Add(printfn "Exception: %A")
counter.Post(42) 

2
当然可以实现。我不明白为什么“默认”行为如此无声。例如,在MailboxProcessor.add_Error中没有处理程序的情况下,它可以重新抛出异常。 调试异步/多线程代码很困难。我们为什么要让这个任务变得更加困难呢? - qehgt
2
你可以认为,如果没有附加处理程序,将取消进程(未处理的异常)会更好。我不记得当时的理由。 - Brian
1
重新抛出异常的一个问题是你会失去堆栈跟踪 - 例如 async{failwith "bad"} 给出了一个指向 F# 库深处的堆栈跟踪,这可能会让调试变得很烦人 - 显然这是 .NET 在不同线程上重新抛出异常的限制。 - John Palmer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接