Proposal: Give weak references the ability to notify reference-holders when they transition to nil


(Michael Henson) #1

The use-case for this comes first from proposals to have a weak-reference
version of collection types. Implementing a notification signal of some
sort to weak reference-holders when the reference becomes nil would make
implementing those more straightforward.

It would also enable implementing cascading-weakness (or propagating
weakness):

class Thing {
  var text: String
}

class ThingProxy {
  weak var thing: Thing?
}

class ContrivedExample {
  weak var proxy: ThingProxy?
}

var example = ContrivedExample()

such that when example.proxy.thing becomes nil, and the ThingProxy instance
is no longer meaningful, example.proxy becomes nil as well.

SInce this is the germ of an idea, I'll avoid suggesting a syntax for the
mechanism so the discussion focuses on whether or not this is useful enough
as a feature to be implemented.

Mike


(Chris Lattner) #2

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

+1. This is very useful for various kinds of APIs, like a weak hashtable that wants to remove the keys when/if they get deallocated.

SInce this is the germ of an idea, I'll avoid suggesting a syntax for the mechanism so the discussion focuses on whether or not this is useful enough as a feature to be implemented.

IMO, ideally, this would be more of a runtime API than a language feature.

-Chris

···

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:


(Greg Parker) #3

How do you want this to work in the presence of threads?

One option is that the nil transition and the callbacks are performed together, synchronously and atomically with respect to some things. The problem with this scheme is that the callback is limited in what it can do. If it does the wrong thing it will deadlock. The definition of "wrong thing" depends in part on the definition of "atomically with respect to some things". For example, if the callbacks are called atomically with respect to other weak reference writes then the callback must not store to any weak references of its own.

Another option is that the callbacks are performed asynchronously some time after the nil transition itself. (Java's PhantomReference offers something like this.) The problem with this scheme is that the state of the world has moved on by the time the callback is called, which can make the callback difficult to write. In particular there is no guarantee that the weak variable's storage still exists when the callback for that weak variable is executed.

···

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

--
Greg Parker gparker@apple.com Runtime Wrangler


(Paul Cantrell) #4

Speak of the devil:

https://github.com/bustoutsolutions/siesta/blob/master/Source/Support/WeakCache.swift#L52
https://github.com/bustoutsolutions/siesta/blob/master/Source/ResourceObserver.swift#L299-L300

Both of these spots leave zombie entires in collections when weakly referenced objects go away (cached resources in the first link, observer owners in second). Siesta gets away with it only because a low memory event triggers a cleanup.

So yeah, this would be useful in the wild.

Not sure about this, but … perhaps Swift could get away with providing this only for properties and not for local variables? If so, might this fold into Joe Groff’s forthcoming property decoration mechanism?

Cheers, P

–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
https://innig.net@inthehandshttp://siestaframework.com/

···

On Dec 13, 2015, at 9:46 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

+1. This is very useful for various kinds of APIs, like a weak hashtable that wants to remove the keys when/if they get deallocated.


(John McCall) #5

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

+1. This is very useful for various kinds of APIs, like a weak hashtable that wants to remove the keys when/if they get deallocated.

Yes. This was always part of the long-term vision for weak references.

SInce this is the germ of an idea, I'll avoid suggesting a syntax for the mechanism so the discussion focuses on whether or not this is useful enough as a feature to be implemented.

IMO, ideally, this would be more of a runtime API than a language feature.

We wouldn’t want to encumber every weak reference with the ability to support having callbacks dynamically registered on it. So yeah, I think we’d want to provide some runtime functions that would be used to implement some stdlib API.

John.

···

On Dec 13, 2015, at 7:46 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:


(Michael Henson) #6

Something occurred to me while I was thinking through a response to the
thread issue. The language already provides a couple of things that behave
similarly:

* deinit methods
* property setters

I couldn't find a documented guarantee for either of them to run on a
particular thread, or any explicit detail on guarantees of behavior in
threaded environments.

Given that every weak reference has to be an Optional type, I ran the
following test code to see if setters are called when a weak reference
becomes nil:

// swift-2.2-SNAPSHOT-2015-12-10-a-ubuntu15.10
import Glibc

class Beeper {
  func beep() {
    print("Beep")
  }
}

class Holder {
  weak var beeper: Beeper? {
    willSet {
      print("willSet: \(newValue)")
    }
    didSet {
      print("didSet: \(beeper)")
    }
  }

  func doExampleLogic() {
    // so there was, at one time, a strong reference in this context
    let beeper = Beeper()
    self.beeper = beeper
  }
}

let holder = Holder()
holder.doExampleLogic()

for i in 0..<15 {
  print("\(i):")

  if let heldBeeper = holder.beeper {
    print("held beeper exists")
  } else {
    print("held beeper is nil")
  }
}

Results:
$ swift beeper.swift
willSet: Optional(beeper.Beeper)
didSet: Optional(beeper.Beeper)
0:
held beeper exists
1:
held beeper exists
2:
held beeper exists
3:
held beeper exists
4:
held beeper exists
5:
held beeper exists
6:
held beeper exists
7:
held beeper exists
8:
held beeper exists
9:
held beeper exists
10:
held beeper exists
11:
held beeper exists
12:
held beeper exists
13:
held beeper exists
14:
held beeper exists

I see the exact same results if I compile with swiftc. I expected the
weakened optional to set itself to nil when the strong reference went out
of scope at the end of doExampleLogic(). Have I misunderstood how weakening
works?

Mike

···

On Mon, Dec 14, 2015 at 1:06 PM, Greg Parker <gparker@apple.com> wrote:

> On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution < > swift-evolution@swift.org> wrote:
>
> The use-case for this comes first from proposals to have a
weak-reference version of collection types. Implementing a notification
signal of some sort to weak reference-holders when the reference becomes
nil would make implementing those more straightforward.

How do you want this to work in the presence of threads?

One option is that the nil transition and the callbacks are performed
together, synchronously and atomically with respect to some things. The
problem with this scheme is that the callback is limited in what it can do.
If it does the wrong thing it will deadlock. The definition of "wrong
thing" depends in part on the definition of "atomically with respect to
some things". For example, if the callbacks are called atomically with
respect to other weak reference writes then the callback must not store to
any weak references of its own.

Another option is that the callbacks are performed asynchronously some
time after the nil transition itself. (Java's PhantomReference offers
something like this.) The problem with this scheme is that the state of the
world has moved on by the time the callback is called, which can make the
callback difficult to write. In particular there is no guarantee that the
weak variable's storage still exists when the callback for that weak
variable is executed.

--
Greg Parker gparker@apple.com Runtime Wrangler


(Etan Kissling) #7

Why not both?

Allow synchronous callbacks for people who know what they do
(writing a global exception handler in other languages can also create major issues if you do it the wrong way)
and allow asynchronous callbacks for people who just want to clean up their collection eventually.

Maybe an approach with `willDeinit` and `didDeinit` would be great here, mirroring the already existing `willSet` and `didSet`.

1. Last strong reference removed
2. All `willDeinit` observers are invoked. It is illegal to resurrect an object in these callbacks.
3. All weak references are zeroed.
4. All `didDeinit` observers are invoked, potentially asynchronously.

The `willDeinit` cannot be merged with the `willSet` because of the additional non-resurrection constraints.
The `didDeinit` could be merged with the `didSet`. Although a `didSet` without a preceding `willSet` is kind of strange.

Property would look like this:

weak var x: T? {
    willSet {
        // Not called on automatic zeroing.
    }
    didSet {
        // Not called on automatic zeroing.
    }
    willDeinit {
        // Object is dying.
    }
    didDeinit {
        // Object has died sometime ago.
    }
}

Alternatively, allow object resurrection between the removal of the last strong reference and the completion of all `willDeinit` callbacks.
`willDeinit` becomes advisory in this case, and may be called multiple times. Maybe `willTryToDeinit` would be clearer in the meaning.
Only if all `willTryToDeinit` callbacks completed and the object is not resurrected, the references are zeroed and `didDeinit` callbacks are called.

This way, the developer doesn't need to care about special restrictions that apply to `willDeinit`.

Maybe `unsafeWillDeinit` would be an okay name if you go with the restricted `willDeinit` to scare novices away from that area (similar to pointers).

Etan

···

On 14 Dec 2015, at 22:06, Greg Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

How do you want this to work in the presence of threads?

One option is that the nil transition and the callbacks are performed together, synchronously and atomically with respect to some things. The problem with this scheme is that the callback is limited in what it can do. If it does the wrong thing it will deadlock. The definition of "wrong thing" depends in part on the definition of "atomically with respect to some things". For example, if the callbacks are called atomically with respect to other weak reference writes then the callback must not store to any weak references of its own.

Another option is that the callbacks are performed asynchronously some time after the nil transition itself. (Java's PhantomReference offers something like this.) The problem with this scheme is that the state of the world has moved on by the time the callback is called, which can make the callback difficult to write. In particular there is no guarantee that the weak variable's storage still exists when the callback for that weak variable is executed.

--
Greg Parker gparker@apple.com Runtime Wrangler

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jordan Rose) #8

Hm. I don't know. Allowing arbitrary callbacks during the deinit process is a little scary to me; it means that any API that vends an object publicly can no longer assume that deinitialization is cheap. I mean, deinit can already do arbitrary work, but the creator of the class has full control over what that work is (modulo their superclasses). This feels like something that a class might need to opt into, much like KVO requires opt-in in Swift with 'dynamic'.

If one of the selling points of ARC over GC is "deterministic destruction", there's value in being able to control what happens during that destruction.

Jordan

···

On Dec 14, 2015, at 11:54 , John McCall via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 7:46 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

+1. This is very useful for various kinds of APIs, like a weak hashtable that wants to remove the keys when/if they get deallocated.

Yes. This was always part of the long-term vision for weak references.


(Etan Kissling) #9

The first case where `willDeinit` is not allowed to resurrect the object is probably not really usable.

Code needs to be able to assume that claiming a weakly referenced object is something nonblocking.

This can only be the case if `willDeinit` completes in a timely fashion - probably still too much if you need to call user code.
If it's fast enough, I would strongly suggest some wording like `unsafeWillDeinit` to emphasize that.

In the end, that leaves the `willTryToDeinit` / `didDeinit` concept.

`didDeinit` is essentially the same that we have now, when we post a notification at the end of `deinit`
that contains an identifier of the deallocated object to be removed from dictionaries etc.

`willTryToDeinit` is something invoked when all strong references are removed from the object,
and may be called multiple times if another thread concurrently claims a strong reference to the object,
or if one of the callbacks decides to resurrect the object past its runtime.

Etan

···

On 15 Dec 2015, at 19:10, Etan Kissling <kissling@oberon.ch> wrote:

Why not both?

Allow synchronous callbacks for people who know what they do
(writing a global exception handler in other languages can also create major issues if you do it the wrong way)
and allow asynchronous callbacks for people who just want to clean up their collection eventually.

Maybe an approach with `willDeinit` and `didDeinit` would be great here, mirroring the already existing `willSet` and `didSet`.

1. Last strong reference removed
2. All `willDeinit` observers are invoked. It is illegal to resurrect an object in these callbacks.
3. All weak references are zeroed.
4. All `didDeinit` observers are invoked, potentially asynchronously.

The `willDeinit` cannot be merged with the `willSet` because of the additional non-resurrection constraints.
The `didDeinit` could be merged with the `didSet`. Although a `didSet` without a preceding `willSet` is kind of strange.

Property would look like this:

weak var x: T? {
   willSet {
       // Not called on automatic zeroing.
   }
   didSet {
       // Not called on automatic zeroing.
   }
   willDeinit {
       // Object is dying.
   }
   didDeinit {
       // Object has died sometime ago.
   }
}

Alternatively, allow object resurrection between the removal of the last strong reference and the completion of all `willDeinit` callbacks.
`willDeinit` becomes advisory in this case, and may be called multiple times. Maybe `willTryToDeinit` would be clearer in the meaning.
Only if all `willTryToDeinit` callbacks completed and the object is not resurrected, the references are zeroed and `didDeinit` callbacks are called.

This way, the developer doesn't need to care about special restrictions that apply to `willDeinit`.

Maybe `unsafeWillDeinit` would be an okay name if you go with the restricted `willDeinit` to scare novices away from that area (similar to pointers).

Etan

On 14 Dec 2015, at 22:06, Greg Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

How do you want this to work in the presence of threads?

One option is that the nil transition and the callbacks are performed together, synchronously and atomically with respect to some things. The problem with this scheme is that the callback is limited in what it can do. If it does the wrong thing it will deadlock. The definition of "wrong thing" depends in part on the definition of "atomically with respect to some things". For example, if the callbacks are called atomically with respect to other weak reference writes then the callback must not store to any weak references of its own.

Another option is that the callbacks are performed asynchronously some time after the nil transition itself. (Java's PhantomReference offers something like this.) The problem with this scheme is that the state of the world has moved on by the time the callback is called, which can make the callback difficult to write. In particular there is no guarantee that the weak variable's storage still exists when the callback for that weak variable is executed.

--
Greg Parker gparker@apple.com Runtime Wrangler

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Etan Kissling) #10

Hmm, just because something could be computationally expensive, it doesn't become non-deterministic.
The program itself as a unit still performs the same steps in the same order, every time.
Which is a huge advantage over GC. Still true for a world with deinit observers.

A similar argument could be brought for non-KVO property observers.

weak var someProperty: String? {
    didSet {
         // Mine some Bitcoins.
    }
}

This is possible right now, but noone does this, because convention dictates that property observers should not be computationally expensive.

The main thing that's being proposed here is that you additionally also get a notification when the property changed to nil.

The client of an API could already implement such a notification scheme in certain cases by wrapping the object returned by the API.
Then, performing the expensive operation when the outer object is deinited.
The API couldn't prevent this, unless it returns a handle instead of an object and requires passing tha thandle back to the API with every call.
This way, the API could ensure that the client has no way to know when a handle becomes invalid.

I don't think that property observers should be treated differently, just because the cause for the property change has a different reason.

Right now, the main pain point is that implementing weak collections is really hard.
(i.e. starting a timer that periodically clears the collection of zombies, or responding to low memory conditions,
or wrapping objects, or lazily cleanup when the collection is modified etc.)

Since a collection is something generic, you cannot require all passed-in objects to opt-in to deinit observing, either.

I don't think that KVO strictly requires the dynamic opt-in. If the compiler decides for whatever reasons to use dynamic dispatch,
i.e. because a class descends from NSObject, then KVO could still work by accident even though you have not explicitly opted in.

Etan

···

On 15 Dec 2015, at 19:29, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 14, 2015, at 11:54 , John McCall via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 7:46 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 13, 2015, at 6:24 PM, Michael Henson via swift-evolution <swift-evolution@swift.org> wrote:

The use-case for this comes first from proposals to have a weak-reference version of collection types. Implementing a notification signal of some sort to weak reference-holders when the reference becomes nil would make implementing those more straightforward.

+1. This is very useful for various kinds of APIs, like a weak hashtable that wants to remove the keys when/if they get deallocated.

Yes. This was always part of the long-term vision for weak references.

Hm. I don't know. Allowing arbitrary callbacks during the deinit process is a little scary to me; it means that any API that vends an object publicly can no longer assume that deinitialization is cheap. I mean, deinit can already do arbitrary work, but the creator of the class has full control over what that work is (modulo their superclasses). This feels like something that a class might need to opt into, much like KVO requires opt-in in Swift with 'dynamic'.

If one of the selling points of ARC over GC is "deterministic destruction", there's value in being able to control what happens during that destruction.

Jordan
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution