Weak Closures

php.internals

Dominic Grostate

4 years ago
Hi Internals, I'd like to express my interest in a possible feature concerning weak references. Currently closures created within a class appear to contain a reference to the object that created it. This is of course necessary in order for the closure to retain the necessary scope. However I would like to suggest we have the option for closure to weakly reference this object so that when the object is garbage collected, the closure too may be rendered null or invalid (inspectable). Consider the following example: https://gist.github.com/orolyn/7651e4127759aad1736547490baa1394 The idea here is that without unset($sample) the loop would run forever, because there is a reference to the callback from the object, and in turn there is a reference to the object from the main stack. With the unset($sample), the idea is that before the callback is even called, the reference to the object is destroyed thusly so is the reference to callback, and since the callback is only referenced now in a WeakMap, the callback will finally be gone. In reality it appears there is a circular reference between the object and the callback. I don't know if this is a bug or not, because from what I can find PHP was fixed a long time ago to resolve circular references. However if this is intentional, I would like the option to make the closure weak for example: $c = WeakClosure::fromCallable(function () {}); $c = Closure::fromCallable(function () {}, true); Please let me know your thoughts, because maybe there is a way of achieving this with PHP as is. Kind regards, Dominic

Christoph Becker

4 years ago
On 21.01.2022 at 11:31, Dominic Grostate wrote:
> I'd like to express my interest in a possible feature concerning weak > references. Currently closures created within a class appear to > contain a reference to the object that created it. This is of course > necessary in order for the closure to retain the necessary scope. > However I would like to suggest we have the option for closure to > weakly reference this object so that when the object is garbage > collected, the closure too may be rendered null or invalid > (inspectable). > > Consider the following example: > https://gist.github.com/orolyn/7651e4127759aad1736547490baa1394 > > The idea here is that without unset($sample) the loop would run > forever, because there is a reference to the callback from the object, > and in turn there is a reference to the object from the main stack. > With the unset($sample), the idea is that before the callback is even > called, the reference to the object is destroyed thusly so is the > reference to callback, and since the callback is only referenced now > in a WeakMap, the callback will finally be gone. > > In reality it appears there is a circular reference between the object > and the callback. I don't know if this is a bug or not, because from > what I can find PHP was fixed a long time ago to resolve circular > references. > > However if this is intentional, I would like the option to make the > closure weak for example: > > $c = WeakClosure::fromCallable(function () {}); > $c = Closure::fromCallable(function () {}, true); > > Please let me know your thoughts, because maybe there is a way of > achieving this with PHP as is.
If you trigger the garbage collector manually (i.e. call gc_collect_cycles() after unset($callback)), the loop terminates right away. I'm not sure why it doesn't without manually triggering the GC. Christoph

Rowan Collins

4 years ago
On 22/01/2022 15:30, Christoph M. Becker wrote:
> If you trigger the garbage collector manually (i.e. call > gc_collect_cycles() after unset($callback)), the loop terminates right > away. I'm not sure why it doesn't without manually triggering the GC.
Most values are freed as soon as their refcount reaches zero, which obviously won't happen for circular references, so an additional algorithm is needed for those. This "cycle collection" algorithm is relatively expensive, so doesn't run every time a possible candidate is found, but only when a list of candidates reaches a particular threshold. The algorithm is outlined in the manual, although it looks like the constant 10000 mentioned there has been replaced by an adaptive threshold (which can be inspected with gc_status()): https://www.php.net/manual/en/features.gc.collecting-cycles.php If you measure memory usage while running the example code in a loop, you should see it slowly growing and then periodically dropping each time a cycle collection is run. That's why gc_collect_cycles() exists - if you _know_ you've created circular references, you can tell the engine to find and free them immediately, rather than waiting for the next pass. Regards,
-- Rowan Tommins [IMSoP]

Christoph Becker

4 years ago
On 22.01.2022 at 17:18, Rowan Tommins wrote:
> On 22/01/2022 15:30, Christoph M. Becker wrote: > >> If you trigger the garbage collector manually (i.e. call >> gc_collect_cycles() after unset($callback)), the loop terminates right >> away.  I'm not sure why it doesn't without manually triggering the GC. > > Most values are freed as soon as their refcount reaches zero, which > obviously won't happen for circular references, so an additional > algorithm is needed for those. This "cycle collection" algorithm is > relatively expensive, so doesn't run every time a possible candidate is > found, but only when a list of candidates reaches a particular > threshold. The algorithm is outlined in the manual, although it looks > like the constant 10000 mentioned there has been replaced by an adaptive > threshold (which can be inspected with gc_status()): > https://www.php.net/manual/en/features.gc.collecting-cycles.php > > If you measure memory usage while running the example code in a loop, > you should see it slowly growing and then periodically dropping each > time a cycle collection is run. That's why gc_collect_cycles() exists - > if you _know_ you've created circular references, you can tell the > engine to find and free them immediately, rather than waiting for the > next pass.
Ah, right. Thanks Rowan! Christoph

Dominic Grostate

4 years ago
That's very strange, I had thought it might be a bug. For simple use cases where an object references it's own closure it might be a memory leak, but for complex event loops it potentially allows unnecessary code to run indefinitely. I'm afraid I don't know any more than that. Thanks for the tip though. On Sat, 22 Jan 2022, 15:30 Christoph M. Becker, <cmbecker69@gmx.de> wrote:

Aaron Piotrowski

4 years ago
> On Jan 21, 2022, at 4:31 AM, Dominic Grostate <codekestrel@googlemail.com> wrote: > > Hi Internals, > > I'd like to express my interest in a possible feature concerning weak > references. Currently closures created within a class appear to > contain a reference to the object that created it. This is of course > necessary in order for the closure to retain the necessary scope. > However I would like to suggest we have the option for closure to > weakly reference this object so that when the object is garbage > collected, the closure too may be rendered null or invalid > (inspectable). > > Consider the following example: > https://gist.github.com/orolyn/7651e4127759aad1736547490baa1394 > > The idea here is that without unset($sample) the loop would run > forever, because there is a reference to the callback from the object, > and in turn there is a reference to the object from the main stack. > With the unset($sample), the idea is that before the callback is even > called, the reference to the object is destroyed thusly so is the > reference to callback, and since the callback is only referenced now > in a WeakMap, the callback will finally be gone. > > In reality it appears there is a circular reference between the object > and the callback. I don't know if this is a bug or not, because from > what I can find PHP was fixed a long time ago to resolve circular > references. > > However if this is intentional, I would like the option to make the > closure weak for example: > > $c = WeakClosure::fromCallable(function () {}); > $c = Closure::fromCallable(function () {}, true); > > Please let me know your thoughts, because maybe there is a way of > achieving this with PHP as is. > > Kind regards, > Dominic > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php >
Hi Dominic, Implementing a weak closure is possible in user code. We've done so in Amp v3: https://github.com/amphp/amp/blob/379177aba93518e2df6d626677cbbdc48cc0d8ae/src/functions.php#L119-L171 I would be in favor of having this functionality available directly in PHP. Amp often uses references to object properties in static closures to avoid circular references to $this. There are other ways to accomplish this, such as a ref object like https://github.com/azjezz/psl/blob/21bf0cd3d6d6055fc88541e9b24f3140bd179b2d/src/Psl/Ref.php, but a weak-ref to $this would certainly be more convenient. Cheers, Aaron Piotrowski

Dominic Grostate

4 years ago
Yes while experimenting I found that solution to work. In order to support $this->func(...) Though I had to use reflection to separate the object and method name. On Sat, 22 Jan 2022, 16:12 Aaron Piotrowski, <aaron@trowski.com> wrote: