MailboxProcessor 在完成期间崩溃

MailboxProcessor crashes during Finalize

此代码在 Mono (5.4.1.7) 上 运行。

我正在使用 F# 的代理来处理我的 Web 应用程序中的大量数据处理,其中一条消息是 Shutdown。当处理发布的关闭消息时,代理会清理一些东西并停止其消息循环。这工作得很好而且花花公子,但如果我尝试从 Finalize() 执行关机,我会大吃一惊。我成功重现了这个:

open System
open System.Threading

type ConsoleMessage =
    | Clear
    | Println of string
    // Reply back (with unit) so that calling code is able to wait for the agent to clean up (for code dependent on the
    // agent's resources definitely being released and such)
    | Shutdown of AsyncReplyChannel<unit>

type ConsoleAgent() =
    let mutable disposed = false
    let mutable stopped = false

    let agent = MailboxProcessor.Start(fun agent ->
        let rec messageLoop () = async {
            let! message = agent.Receive ()
            match message with
            | Clear -> System.Console.Clear ()
            | Println str -> printfn "%s" str
            | Shutdown rc ->
                // Cleanup goes here
                printfn "Shutting Down"
                stopped <- true
                rc.Reply ()
            System.Threading.Thread.Sleep 100
            if not stopped then
                return! messageLoop () }
        messageLoop ())

    member this.Post msg = agent.Post msg

    member this.PostAndAsyncReply msg = agent.PostAndAsyncReply msg

    member this.Dispose disposing =
        printfn "Disposing (disposing = %b)" disposing
        if not disposed then
            Async.RunSynchronously (agent.PostAndAsyncReply Shutdown)
            disposed <- true

    override this.Finalize () =
        this.Dispose false

    interface IDisposable with
        member this.Dispose () =
            this.Dispose true

module Main =
    [<EntryPoint>]
    let main args =
        let console = new ConsoleAgent()
        console.Post (Println "Print 1")
        console.Post (Println "Print 2")
        Thread.Sleep 1000
        0

当然在实际应用中它们与控制台打印无关。

这是我得到的堆栈跟踪:

Unhandled Exception:
System.NullReferenceException: Object reference not set to an instance of an object
  at System.Runtime.Remoting.Contexts.SynchronizationAttribute.EnterContext () [0x00000] in /Users/builder/data/lanes/4992/mono-mac-sdk/external/bockbuild/builds/mono-x64/mcs/class/corlib/System.Runtime.Remoting.Contexts/SynchronizationAttribute.cs:184 
  at System.Threading.WaitHandle.WaitOneNative (System.Runtime.InteropServices.SafeHandle waitableSafeHandle, System.UInt32 millisecondsTimeout, System.Boolean hasThreadAffinity, System.Boolean exitContext) [0x0002d] in /Users/builder/data/lanes/4992/mono-mac-sdk/external/bockbuild/builds/mono-x64/mcs/class/corlib/System.Threading/WaitHandle.cs:111 
  at System.Threading.WaitHandle.InternalWaitOne (System.Runtime.InteropServices.SafeHandle waitableSafeHandle, System.Int64 millisecondsTimeout, System.Boolean hasThreadAffinity, System.Boolean exitContext) [0x00014] in /Users/builder/data/lanes/4992/mono-mac-sdk/external/bockbuild/builds/mono-x64/mcs/class/referencesource/mscorlib/system/threading/waithandle.cs:250 
  at System.Threading.WaitHandle.WaitOne (System.Int64 timeout, System.Boolean exitContext) [0x00000] in /Users/builder/data/lanes/4992/mono-mac-sdk/external/bockbuild/builds/mono-x64/mcs/class/referencesource/mscorlib/system/threading/waithandle.cs:239 
  at System.Threading.WaitHandle.WaitOne (System.Int32 millisecondsTimeout, System.Boolean exitContext) [0x00019] in /Users/builder/data/lanes/4992/mono-mac-sdk/external/bockbuild/builds/mono-x64/mcs/class/referencesource/mscorlib/system/threading/waithandle.cs:206 
  at Microsoft.FSharp.Control.AsyncImpl+ResultCell`1[T].TryWaitForResultSynchronously (Microsoft.FSharp.Core.FSharpOption`1[T] timeout) [0x0002a] in <5a7d678a904cf4daa74503838a677d5a>:0 
  at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronouslyInCurrentThread[a] (System.Threading.CancellationToken token, Microsoft.FSharp.Control.FSharpAsync`1[T] computation) [0x0001c] in <5a7d678a904cf4daa74503838a677d5a>:0 
  at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously[a] (System.Threading.CancellationToken token, Microsoft.FSharp.Control.FSharpAsync`1[T] computation, Microsoft.FSharp.Core.FSharpOption`1[T] timeout) [0x00013] in <5a7d678a904cf4daa74503838a677d5a>:0 
  at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T] (Microsoft.FSharp.Control.FSharpAsync`1[T] computation, Microsoft.FSharp.Core.FSharpOption`1[T] timeout, Microsoft.FSharp.Core.FSharpOption`1[T] cancellationToken) [0x00070] in <5a7d678a904cf4daa74503838a677d5a>:0 
  at Program+ConsoleAgent.Dispose (System.Boolean disposing) [0x00027] in /Users/jwostenberg/Code/FSharp/Sandbox/Sandbox/Program.fs:38 
  at Program+ConsoleAgent.Finalize () [0x00000] in /Users/jwostenberg/Code/FSharp/Sandbox/Sandbox/Program.fs:42 

此外,如果对象通过处置模式正确处置(例如,将 let console = new ConsoleAgent() 更改为 use console = new ConsoleAgent()),则不会发生这种情况。我不能在我自己的代码中真正做到这一点而不向后弯腰,因为我没有直接引用这些代理(一次有很多 运行),但我不应该能够还是让他们通过垃圾收集器处理?

这是我的错、F# 的错还是 Mono 的错?现在,我已经将 Dispose() 方法的相关部分包装在一个只记录异常的 try/catch 中,但这感觉真的很脏。

很少有场景需要覆盖 Finalize,而且您的用例似乎不适用。请参阅 "Notes for implementers" 部分 here and this entire article

The Object.Finalize method does nothing by default, but you should override Finalize only if necessary, and only to release unmanaged resources.

回复:您的评论:

How do you ensure that the MailboxProcessor message loop is shut down without Finalize?

您可以只使用 IDisposable 模式,或者更明确地管理 MailboxProcessor 的生命周期,这可能需要重构您的项目。

I can't really do this in my own code without bending over backwards because I don't have direct references to these agents (there are many of them running at a time), but shouldn't I be able to let them dispose via the garbage collector anyways?

是的,假设他们不拥有非托管资源,您应该能够让他们处置 "naturally"。如果没有看到 real-world 用例,很难说,但听起来您希望对处理器的生命周期有更多的控制。这可能有点像 XY problem.

方法Dispose的参数"disposing"在这里是有原因的。它区分了 Dispose 的托管和非托管应用程序。简而言之,Dispose(true) 意味着此调用是显式的(使用语句或 F# 的 use)。它基本上是 "normal" .NET 编程的延续。

Dispose(false) 表示正在完成。这意味着任何引用的 .NET 对象都可以是活动的、已处置的或已完成的。因此,您的代码只需要关心非托管资源,不要尝试以任何其他方式调用或使用托管对象。

重要的是,Dispose() 不会自动调用,而终结器会自动调用。使示例正确需要两个更改:

  • 显式控制一次性对象的状态
  • 仅在对象被释放时发送消息,未完成

代码:

    member this.Dispose disposing =
        if disposing && not disposed then
            Async.RunSynchronously (agent.PostAndAsyncReply Shutdown)
            disposed <- true

module Main =
    [<EntryPoint>]
    let main args =
        use console = new ConsoleAgent()
        Thread.Sleep 1000
        0