Сбой MailboxProcessor во время финализации

Этот код работает на Mono (5.4.1.7).

Я использую Агенты F# для обработки большого количества данных в моем веб-приложении, и одно из этих сообщений - Завершение работы. Когда обработанное сообщение о завершении работы обрабатывается, агент очищает некоторые вещи и останавливает цикл обработки сообщений. Это работает отлично и денди, но взрывается мне в лицо, если я пытаюсь выполнить выключение с 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# или Моно? На данный момент я обернул соответствующую часть метода Dispose() в try/catch, который просто регистрирует исключение, но это действительно грязно.

2 ответа

Решение

Аргумент "удаление" метода Dispose приведен здесь по причине. Различает управляемые и неуправляемые приложения Dispose. Вкратце, Dispose(true) означает, что этот вызов является явным (используя оператор или F#). use). Это в основном продолжение "нормального" .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

Есть очень немного сценариев, где вам нужно переопределить Finalize и это не похоже на ваш вариант использования. Смотрите раздел "Примечания для разработчиков" и всю эту статью.

Object.Finalize Метод по умолчанию ничего не делает, но вы должны переопределить Finalize только в случае необходимости и только для освобождения неуправляемых ресурсов.

Re: ваш комментарий:

Как вы убедитесь, что MailboxProcessor Цикл сообщений отключен без Finalize?

Вы можете просто использовать IDisposable или управлять временем жизни вашего MailboxProcessor Более четко, что может потребовать рефакторинга вашего проекта.

Я действительно не могу сделать это в своем собственном коде, не перегибаясь в обратном направлении, потому что у меня нет прямых ссылок на этих агентов (их много одновременно), но я не должен позволять им распоряжаться через сборщик мусора в любом случае?

Да, вы должны позволить им распоряжаться "естественно", если они не владеют неуправляемыми ресурсами. Трудно сказать, не видя реального варианта использования, но кажется, что вы хотите больше контролировать время жизни процессоров. Это может быть отчасти проблемой XY.

Другие вопросы по тегам