Do F# observable events obviate, mediate, or are n

2019-04-28 12:28发布

Since observables are typically IDisposable how does that change, if at all, the need to use weak references in event handlers, or any other event based memory leak/GC locked referencing?

While my primary concern/need is for WPF I'm looking for the broader example and trying to understand where I may need weak references.

F#'s Observable.add doesn't provide a way to unhook the event, so I'm thinking it's less likely to be a source of leaks. Sample code:

type Notifier() = 
    let propChanged = new Event<_,_>()
    member __.Foo() = ()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member __.PropertyChanged = propChanged.Publish
    abstract member RaisePropertyChanged : string -> unit
    default x.RaisePropertyChanged(propertyName : string) = propChanged.Trigger(x, PropertyChangedEventArgs(propertyName))


Notifier() :?> INotifyPropertyChanged
|> Observable.add(fun _ -> printfn "I'm hooked on you")

1条回答
Melony?
2楼-- · 2019-04-28 13:24

F#'s Observable.add doesn't provide a way to unhook the event, so I'm thinking it's less likely to be a source of leaks

It's actually the opposite. Observable.add, by the docs, permanently subscribes to the event, and forces a "leak". It's effectively doing an event handler addition that has no way to unsubscribe.

In general, with Observable (in F# and C#), you should favor using .subscribe, and disposing of the subscription handle when you're done.

As @rmunn mentioned, Gjallarhorn can serve as an alternative to using observables in some scenarios (and integrates nicely with them as needed). While writing it, one of my main goals was to make it so that subscriptions don't leak - all of the subscriptions use a hybrid push/pull model based on weak references, which prevents many of the problems with leaking in event and observable based code.

To demonstrate, I've thrown together a variation on your code, using both observables and Gjallarhorn's signals. If you run this in a release build, outside of the debugger, you'll see the difference:

type Notifier() = 
    let propChanged = new Event<_,_>()
    member __.Foo() = ()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member __.PropertyChanged = propChanged.Publish
    abstract member RaisePropertyChanged : string -> unit
    default x.RaisePropertyChanged(propertyName : string) = propChanged.Trigger(x, PropertyChangedEventArgs(propertyName))

let obs () =
    use mre = new ManualResetEvent(false)

    let not = Notifier()

    do       
       let inpc = not :> INotifyPropertyChanged
       inpc.PropertyChanged 
       |> Observable.add (fun p -> printfn "Hit %s!" p.PropertyName)       

       async {
            for i in [0 .. 10] do
                do! Async.Sleep 100
                printfn "Raising"
                not.RaisePropertyChanged <| sprintf "%d" i
            mre.Set () |> ignore
       } |> Async.Start

       printfn "Exiting block"

    GC.Collect() // Force a collection, to "cleanup"
    mre.WaitOne() |> ignore

let signals () =
    use mre = new ManualResetEvent(false)

    let not = Mutable.create 0

    do
       not 
       |> Signal.Subscription.create (fun v -> printfn "Hit %d!" v)
       |> ignore // throw away subscription handle

       async {
            for i in [0 .. 10] do
                do! Async.Sleep 100 
                printfn "Setting"
                not.Value <- i                
            mre.Set () |> ignore
       } |> Async.Start

       printfn "Exiting block"

    GC.Collect() // Force a collection, to "cleanup"
    mre.WaitOne() |> ignore


[<STAThread>]
[<EntryPoint>]
let main _ =
    printfn "Using observable"
    obs ()

    printfn "Using signals"
    signals ()

    1

Note that both do something similar - they create a "source", then, in a separate scope, subscribe to it and throw away the disposable subscription handle (Observable.add is nothing but subscribe |> ignore - see code for details.). When running in a release build outside of the debugger (the debugger prevents cleanup from happening), you see:

Using observable
Exiting block
Raising
Hit 0!
Raising
Hit 1!
Raising
Hit 2!
Raising
Hit 3!
Raising
Hit 4!
Raising
Hit 5!
Raising
Hit 6!
Raising
Hit 7!
Raising
Hit 8!
Raising
Hit 9!
Raising
Hit 10!
Using signals
Exiting block
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Press any key to continue . . .

In the observable case, the call to .add permanently holds a reference to the notifier, preventing it from being garbage collected. With signals, the signal subscription will GC, and "unhook" automatically, preventing the calls from Hit from ever being displayed.

查看更多
登录 后发表回答