Сбой 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.