In gsi, a will that has become unreachable will still execute when its testator is about to become unreachable.
What's more, if the will's action is a closure upon some other object, and if we make that other object unreachable too (such that a different will with that other object as a testator executes), when the action executes it will still be able to access that other object. This is probably clearer in code:
(define o1 (cons 'o 1)) (define o2 (cons 'o 2)) (define w1 (make-will o1 (let ((o2_ o2)) (lambda (x) (println "w1 says
bye to " x " but it can still reach " o2_)))))
(define w2 (make-will o2 (lambda (x) (println "w2 says bye to o2")))) (define w3 (make-will w1 (lambda (x) (println "w3 says bye to w1")))) (set! w1
#f)
(gc)
w3 says bye to w1
(set! o2 #f) (gc)
w2 says bye to o2
(set! o1 #f) (gc)
w1 says bye to o1 but it can still reach o2
Is this expected semantics, or just accidental behavior that shouldn't be relied upon?
If o2 was a foreign object with a finalizer, would it have been released by the time w1 executed, or would w1's closure reference have kept it alive?
Afficher les réponses par date
On Dec 21, 2013, at 9:33 PM, Estevo euccastro@gmail.com wrote:
In gsi, a will that has become unreachable will still execute when its testator is about to become unreachable.
What's more, if the will's action is a closure upon some other object, and if we make that other object unreachable too (such that a different will with that other object as a testator executes), when the action executes it will still be able to access that other object. This is probably clearer in code:
(define o1 (cons 'o 1)) (define o2 (cons 'o 2)) (define w1 (make-will o1 (let ((o2_ o2)) (lambda (x) (println "w1 says bye to " x " but it can still reach " o2_))))) (define w2 (make-will o2 (lambda (x) (println "w2 says bye to o2")))) (define w3 (make-will w1 (lambda (x) (println "w3 says bye to w1")))) (set! w1 #f) (gc)
w3 says bye to w1
(set! o2 #f) (gc)
w2 says bye to o2
(set! o1 #f) (gc)
w1 says bye to o1 but it can still reach o2
Is this expected semantics, or just accidental behavior that shouldn't be relied upon?
It is expected, in the sense that it is consistent with the rule that a will becomes executable if its testator is not strongly reachable. Here, after (set! o2 #f), o2 is not strongly reachable. The will w1 isn’t strongly reachable, so w1’s action closure isn’t strongly reachable, so even if its free variable o2_ contains a pointer to o2, there isn’t a strong chain from a root which reaches o2.
If o2 was a foreign object with a finalizer, would it have been released by the time w1 executed, or would w1's closure reference have kept it alive?
The GC only calls the finalizer (release function?) when it reclaims the space of the object. So as long as you can get your hands on o2 (when w1’s action procedure is executed), the finalizer will not have been called.
Marc
Thanks for the detailed explanation.
The GC only calls the finalizer (release function?) when it reclaims the
space of the object.
So as long as you can get your hands on o2 (when w1’s action procedure is
executed), the
finalizer will not have been called.
After w1 and o2 have been set to #f, is there anything preventing the space for either of them to be reclaimed anytime? In particular, - is the fact that o1 hasn't been reclaimed yet keeping w1 alive? - will the reference to o2 in w1's action closure prevent it from being reclaimed before that closure is reclaimed?
A corollary to the latter: if I delay the execution of w1's action closure for long enough (e.g. by holding onto o1) could I ever get it to execute with o2_ pointing to freed/invalid memory?
On Dec 22, 2013, at 9:35 PM, Estevo euccastro@gmail.com wrote:
Thanks for the detailed explanation.
The GC only calls the finalizer (release function?) when it reclaims the space of the object. So as long as you can get your hands on o2 (when w1’s action procedure is executed), the finalizer will not have been called.
After w1 and o2 have been set to #f, is there anything preventing the space for either of them to be reclaimed anytime? In particular,
- is the fact that o1 hasn't been reclaimed yet keeping w1 alive?
- will the reference to o2 in w1's action closure prevent it from being reclaimed before that closure is reclaimed?
A corollary to the latter: if I delay the execution of w1's action closure for long enough (e.g. by holding onto o1) could I ever get it to execute with o2_ pointing to freed/invalid memory?
Lets define some concepts more formally:
1) reachability path of an object: a chain of references from the roots that reaches the object (these references can be a mixture of weak and strong references)
2) weak reachability of an object: all the reachability paths of the object contain at least one weak reference
3) strong reachability of an object: at least one of the reachability paths of the object contain only strong references
4) reachability of an object: an object that is either weakly reachable or strongly reachable
Note that during execution, an object’s reachability may alternate between weakly reachable and strongly reachable (any number of times, as required by the program).
The action procedure of a will is executed when the GC detects that the will’s testator object is weakly reachable. The GC is *not* taking the decision to reclaim the testator object, in fact it can’t reclaim the object since it is (at least) required to be passed as a parameter to the action procedure.
The GC reclaims objects that are not reachable. When a foreign object is reclaimed by the GC, its release function is called (which may cause other “finalization” actions, such as reclaiming space on the C heap). Note that it is possible to force the execution of the release function by explicitly calling (foreign-release! obj). Those are the only situations when the release function is called. So if you don’t use foreign-release!, the Scheme code manipulating a foreign object has the guarantee that the release function hasn’t been called (regardless of whether the object is the testator of a will or not).
Marc
Thanks again Marc, this is great! My misunderstanding was that I thought objects could be reclaimed anytime they are not _strongly_ reachable anymore. I think this is how it works in Python (any use of weak references to reclaimed objects raises a ReferenceError). Gambit's approach is indeed more powerful, and I can now see how it will simplify a problem I have.
So just to confirm: inclusion of a will in the executable or nonexecutable will lists is/works like a weak reference, right? In the example I gave before, w1 couldn't be reached weakly or strongly from the toplevel at the point its action procedure got executed.
On Mon, Dec 23, 2013 at 3:14 PM, Marc Feeley feeley@iro.umontreal.cawrote:
On Dec 22, 2013, at 9:35 PM, Estevo euccastro@gmail.com wrote:
Thanks for the detailed explanation.
The GC only calls the finalizer (release function?) when it reclaims
the space of the object.
So as long as you can get your hands on o2 (when w1’s action procedure
is executed), the
finalizer will not have been called.
After w1 and o2 have been set to #f, is there anything preventing the
space for either of them to be reclaimed anytime? In particular,
- is the fact that o1 hasn't been reclaimed yet keeping w1 alive?
- will the reference to o2 in w1's action closure prevent it from
being reclaimed before that closure is reclaimed?
A corollary to the latter: if I delay the execution of w1's action
closure for long enough (e.g. by holding onto o1) could I ever get it to execute with o2_ pointing to freed/invalid memory?
Lets define some concepts more formally:
- reachability path of an object: a chain of references from the roots
that reaches the object (these references can be a mixture of weak and strong references)
- weak reachability of an object: all the reachability paths of the
object contain at least one weak reference
- strong reachability of an object: at least one of the reachability
paths of the object contain only strong references
- reachability of an object: an object that is either weakly reachable or
strongly reachable
Note that during execution, an object’s reachability may alternate between weakly reachable and strongly reachable (any number of times, as required by the program).
The action procedure of a will is executed when the GC detects that the will’s testator object is weakly reachable. The GC is *not* taking the decision to reclaim the testator object, in fact it can’t reclaim the object since it is (at least) required to be passed as a parameter to the action procedure.
The GC reclaims objects that are not reachable. When a foreign object is reclaimed by the GC, its release function is called (which may cause other “finalization” actions, such as reclaiming space on the C heap). Note that it is possible to force the execution of the release function by explicitly calling (foreign-release! obj). Those are the only situations when the release function is called. So if you don’t use foreign-release!, the Scheme code manipulating a foreign object has the guarantee that the release function hasn’t been called (regardless of whether the object is the testator of a will or not).
Marc
On Dec 23, 2013, at 11:10 AM, Estevo euccastro@gmail.com wrote:
Thanks again Marc, this is great! My misunderstanding was that I thought objects could be reclaimed anytime they are not _strongly_ reachable anymore. I think this is how it works in Python (any use of weak references to reclaimed objects raises a ReferenceError). Gambit's approach is indeed more powerful, and I can now see how it will simplify a problem I have.
So just to confirm: inclusion of a will in the executable or nonexecutable will lists is/works like a weak reference, right? In the example I gave before, w1 couldn't be reached weakly or strongly from the toplevel at the point its action procedure got executed.
Yes. So to be clearer, Gambit’s runtime system keeps a weak reference to each will created with make-will. The weak reference is severed when the will’s action procedure is executed, making the will unreachable. This allows the will object itself to be reclaimed after the action procedure is executed.
Marc
Yes. So to be clearer, Gambit’s runtime system keeps a weak reference to each will created with make-will. The weak reference is severed when the will’s action procedure is executed, making the will unreachable.
This
allows the will object itself to be reclaimed after the action procedure
is
executed.
Clearer indeed!
Just for the benefit of anyone following this (i.e. this is not a question), I inferred this in the gsi REPL: the weak reference to the will seems to be severed even if the action procedure has made the object reachable again (e.g. by setting a global variable to it). Now, even if you keep a strong reference to the will, its action procedure *won't* execute when the object becomes weakly reachable again.
(define o (cons 'o 'o)) (define w (make-will o (lambda (x) (println "saving o") (set! o x)))) (set! o #f) (##gc)
saving o
(set! o #f) (##gc) w
#<will #3>
On Dec 23, 2013, at 12:51 PM, Estevo euccastro@gmail.com wrote:
Yes. So to be clearer, Gambit’s runtime system keeps a weak reference to each will created with make-will. The weak reference is severed when the will’s action procedure is executed, making the will unreachable. This allows the will object itself to be reclaimed after the action procedure is executed.
Clearer indeed!
Just for the benefit of anyone following this (i.e. this is not a question), I inferred this in the gsi REPL: the weak reference to the will seems to be severed even if the action procedure has made the object reachable again (e.g. by setting a global variable to it). Now, even if you keep a strong reference to the will, its action procedure *won't* execute when the object becomes weakly reachable again.
(define o (cons 'o 'o)) (define w (make-will o (lambda (x) (println "saving o") (set! o x)))) (set! o #f) (##gc)
saving o
(set! o #f) (##gc) w
#<will #3>
Will action procedures are “single shot”. When the GC detects that an object has become weakly reachable, it schedules the will’s action procedure to be executed. This happens only once. If you want to detect subsequent transitions from strongly reachable to weakly reachable, just create a new will for this.
Marc
Will action procedures are “single shot”. When the GC detects that an object has become weakly reachable, it schedules the will’s action procedure to be executed. This happens only once. If you want to detect subsequent transitions from strongly reachable to weakly reachable, just create a new will for this.
The problem I'm having is that I wanted to use wills to do something when I knew for sure that an object was never going to be reachable from user code again.
I wanted to fake references between foreign objects, so object A would not be reclaimed as long as object B is reachable from user code.
My plan was/is to make a will with B as testator and a closure on A as action. The action would do nothing[1], but the closure reference would keep A reachable and thus prevent it from being reclaimed.
This would feel safe enough for using in my own code, but I'd be more remiss to offer this as a library for others to use. The scheme is defeated by the fact that as long as B is weakly reachable (e.g. by a will or through a table with weak keys/values), it can be made strongly reachable again by user code in a number of ways. So the user could get in a state where B is reachable but A has been freed. This is a problem if A is a C struct and B is a struct embedded in A.
I think I can solve the original problem in a less expensive way by giving the user ___alloc_rc'ed foreign objects and using the data field in these objects to create the reference to A from B. I'll explore this further now; I was just trying to exhaust what could be done with Scheme code (i.e. with wills) first.
Thank you very much for your clarifications!
[1] Or it would do just enough to prevent the compiler from optimizing out the reference to A; I haven't checked whether this could be a problem yet.