From: Vladimir S. <vs...@gm...> - 2010-10-16 19:40:08
|
Hi, I have some trouble understanding how GC interacts with threads. Here's some example code: (defclass foo () ()) (defvar some-thread (sb-thread:make-thread (lambda () (catch 'done (sb-ext:finalize (make-instance 'foo) (let ((thread sb-thread:*current-thread*)) (lambda () (sb-thread:interrupt-thread thread (lambda () (throw 'done nil)))))) (sleep 1000000))))) (defvar some-thread1 (sb-thread:make-thread (lambda () (catch 'done (sb-ext:finalize (make-instance 'foo) (let ((thread sb-thread:*current-thread*)) (lambda () (sb-thread:interrupt-thread thread (lambda () (throw 'done nil)))))) (loop repeat 10000 do (sleep 1)))))) ;; same, except for this line In either case, the instance of foo is not getting collected until the thread finishes running (as can be seen in the third example below): (let ((s *standard-output*)) (defvar some-thread2 (sb-thread:make-thread (lambda () (catch 'done (sb-ext:finalize (make-instance 'foo) (let ((thread sb-thread:*current-thread*)) (lambda () (format s "~%Getting garbage collected") (sb-thread:interrupt-thread thread (lambda () (format s "~%In interrupt") (throw 'done nil)))))) ;; this obviously throws an error (loop repeat 1 do (sleep 1))))))) The reason I'm trying to do something like this is to have GCable futures that release their underlying threads in the Eager Futures library. Thank you, Vladimir |
From: Vladimir S. <vs...@gm...> - 2010-10-19 15:47:43
|
Just to follow up, Clozure seems to do the expected thing in this scenario. Code: (defclass foo () ()) (defvar some-thread (ccl:process-run-function "some thread" (lambda () (catch 'done (ccl:terminate-when-unreachable (make-instance 'foo) (let ((thread ccl:*current-process*)) (lambda (x) (declare (ignore x)) (ccl:process-interrupt thread (lambda () (throw 'done nil)))))) (sleep 1000000))))) (ccl:gc) (ccl::process-active-p some-thread) => nil Vladimir 2010/10/16 Vladimir Sedach <vs...@gm...>: > Hi, > > I have some trouble understanding how GC interacts with threads. > Here's some example code: > > (defclass foo () ()) > > (defvar some-thread > (sb-thread:make-thread > (lambda () > (catch 'done > (sb-ext:finalize > (make-instance 'foo) > (let ((thread sb-thread:*current-thread*)) > (lambda () > (sb-thread:interrupt-thread > thread > (lambda () (throw 'done nil)))))) > (sleep 1000000))))) > > (defvar some-thread1 > (sb-thread:make-thread > (lambda () > (catch 'done > (sb-ext:finalize > (make-instance 'foo) > (let ((thread sb-thread:*current-thread*)) > (lambda () > (sb-thread:interrupt-thread > thread > (lambda () (throw 'done nil)))))) > (loop repeat 10000 do (sleep 1)))))) ;; same, except for this line > > In either case, the instance of foo is not getting collected until the > thread finishes running (as can be seen in the third example below): > > (let ((s *standard-output*)) > (defvar some-thread2 > (sb-thread:make-thread > (lambda () > (catch 'done > (sb-ext:finalize > (make-instance 'foo) > (let ((thread sb-thread:*current-thread*)) > (lambda () > (format s "~%Getting garbage collected") > (sb-thread:interrupt-thread > thread > (lambda () (format s "~%In interrupt") (throw 'done > nil)))))) ;; this obviously throws an error > (loop repeat 1 do (sleep 1))))))) > > The reason I'm trying to do something like this is to have GCable > futures that release their underlying threads in the Eager Futures > library. > > Thank you, > Vladimir > |
From: Vladimir S. <vs...@gm...> - 2010-10-28 02:32:01
|
To follow up some more, here is an even better example of this behavior: (defun weak-interrupt (ptr) (declare (optimize (debug 3))) (sb-thread:make-thread (lambda () (catch 'done (sb-ext:finalize (sb-ext:weak-pointer-value ptr) (let ((thread sb-thread:*current-thread*)) (lambda () (sb-thread:interrupt-thread thread (lambda () (throw 'done nil)))))) (sleep 1000000))))) (weak-interrupt (sb-ext:make-weak-pointer (make-instance 'foo))) The finalizer never gets called. I suspect this happens because the return value of (sb-ext:weak-pointer-value ptr) is on the stack or in a register so the object never gets collected, but I can't tell from reading the output of disassemble. Can someone take a look and confirm or disprove this hypothesis? Thank you, Vladimir 2010/10/19 Vladimir Sedach <vs...@gm...>: > Just to follow up, Clozure seems to do the expected thing in this scenario. > > Code: > > (defclass foo () ()) > > (defvar some-thread > (ccl:process-run-function "some thread" > (lambda () > (catch 'done > (ccl:terminate-when-unreachable > (make-instance 'foo) > (let ((thread ccl:*current-process*)) > (lambda (x) > (declare (ignore x)) > (ccl:process-interrupt > thread > (lambda () (throw 'done nil)))))) > (sleep 1000000))))) > > (ccl:gc) > > (ccl::process-active-p some-thread) => nil > > Vladimir > > 2010/10/16 Vladimir Sedach <vs...@gm...>: >> Hi, >> >> I have some trouble understanding how GC interacts with threads. >> Here's some example code: >> >> (defclass foo () ()) >> >> (defvar some-thread >> (sb-thread:make-thread >> (lambda () >> (catch 'done >> (sb-ext:finalize >> (make-instance 'foo) >> (let ((thread sb-thread:*current-thread*)) >> (lambda () >> (sb-thread:interrupt-thread >> thread >> (lambda () (throw 'done nil)))))) >> (sleep 1000000))))) >> >> (defvar some-thread1 >> (sb-thread:make-thread >> (lambda () >> (catch 'done >> (sb-ext:finalize >> (make-instance 'foo) >> (let ((thread sb-thread:*current-thread*)) >> (lambda () >> (sb-thread:interrupt-thread >> thread >> (lambda () (throw 'done nil)))))) >> (loop repeat 10000 do (sleep 1)))))) ;; same, except for this line >> >> In either case, the instance of foo is not getting collected until the >> thread finishes running (as can be seen in the third example below): >> >> (let ((s *standard-output*)) >> (defvar some-thread2 >> (sb-thread:make-thread >> (lambda () >> (catch 'done >> (sb-ext:finalize >> (make-instance 'foo) >> (let ((thread sb-thread:*current-thread*)) >> (lambda () >> (format s "~%Getting garbage collected") >> (sb-thread:interrupt-thread >> thread >> (lambda () (format s "~%In interrupt") (throw 'done >> nil)))))) ;; this obviously throws an error >> (loop repeat 1 do (sleep 1))))))) >> >> The reason I'm trying to do something like this is to have GCable >> futures that release their underlying threads in the Eager Futures >> library. >> >> Thank you, >> Vladimir >> > |
From: Nikodemus S. <nik...@ra...> - 2010-10-28 06:49:24
|
On 16 October 2010 22:39, Vladimir Sedach <vs...@gm...> wrote: (Replying an earlier post on the thread because I had a half-written reply for this one already.) > (defclass foo () ()) > > (defvar some-thread > (sb-thread:make-thread > (lambda () > (catch 'done > (sb-ext:finalize > (make-instance 'foo) > (let ((thread sb-thread:*current-thread*)) > (lambda () > (sb-thread:interrupt-thread > thread > (lambda () (throw 'done nil)))))) > (sleep 1000000))))) Two things can easily cause this not to call the finalizer as soon as you expect. 1. If no further consing occurs, no GC will occur, and hence finalizers will not be considered. 2. Even if consing occurred, a leftover pointer to FOO in register or on stack can cause it to be retained. (Or an apparent pointer: SBCL's GC is conservative on stack and registers on x86 and x86-64.) The fact that things happen in an new thread is a red herring here. > (defvar some-thread1 > (sb-thread:make-thread > (lambda () > (catch 'done > (sb-ext:finalize > (make-instance 'foo) > (let ((thread sb-thread:*current-thread*)) > (lambda () > (sb-thread:interrupt-thread > thread > (lambda () (throw 'done nil)))))) > (loop repeat 10000 do (sleep 1)))))) Same thing applies here. > The reason I'm trying to do something like this is to have GCable > futures that release their underlying threads in the Eager Futures > library. That should not be an issue. In a condensed example like this it is very easy to run afoul of conservativism. A more realistic case where finalizers are not getting run would be actually easier to examine sensibly. You should also not that finalizers are not guaranteed to run predictably, or in a timely fashion, but only once the object they are associated with is safely dead -- which may take longer than its logically obvious lifetime, particularly if it has lived long enough to be tenured into a higher generation. Expecting 1 run to allocate N objects with finalizers and having all of those finalizers fire before the next run isn't going to work. A reasonable expectation is each run out of 10000 on __average__ firing N finalizers. I'm uncertain what the issue here is * Are your finalizers not firing, ever? * Are they firing, but not in a sufficiently predictable or timely fashion? ...or something else? Cheers, -- Nikodemus |
From: Vladimir S. <vs...@gm...> - 2010-10-29 03:36:51
|
> Two things can easily cause this not to call the finalizer as soon as > you expect. > > 1. If no further consing occurs, no GC will occur, and hence > finalizers will not be considered. I should have stated that I run (sb-ext:gc :full t) after making sure the object is no longer accessible (clearing * etc). > 2. Even if consing occurred, a leftover pointer to FOO in register or > on stack can cause it to be retained. (Or an apparent pointer: SBCL's > GC is conservative on stack and registers on x86 and x86-64.) That seems to be the case. How can I prevent this from happening on SBCL? > I'm uncertain what the issue here is > > * Are your finalizers not firing, ever? > > * Are they firing, but not in a sufficiently predictable or timely fashion? > > ...or something else? The finalizers only fire once the (sleep 10000) finishes sleeping (what happens then is that probably the stack frame gets popped and the accidental pointer discarded). If in the examples (sleep 10000) is replaced with a computation that never terminates, the thread will never be released even when object foo is not reachable anymore (the fact that there's a pointer on the stack or in a register is an implementation detail, but foo itself is no longer reachable in the semantics of the code - that's the whole point of using weak pointers). This is a real use case of Eager Future - foo would be a future/promise structure, and the computation could either be non-terminating speculative, or something that's sleeping on a semaphore that will never be woken. As I stated before, Clozure produces the expected results - foo is collected, the finalizer gets called, and the non-terminating computation aborted. There's no timing guarantees for when finalizers are called, but in this case it's guaranteed that the finalizer will *never* be called in SBCL, which IMO is a bug. This doesn't happen in all ways that foo can be referenced. For example, special variables do the right thing: (defparameter dynamic-foo (make-instance 'foo)) (let ((s *standard-output*)) (defparameter some-thread3 (sb-thread:make-thread (lambda () (catch 'done (sb-ext:finalize dynamic-foo (let ((thread sb-thread:*current-thread*)) (lambda () (format s "FINALIZING3~%") (sb-thread:interrupt-thread thread (lambda () (throw 'done nil)))))) (sleep 1000000)))))) (setf dynamic-foo nil) (sb-ext:gc :full t) I can't use dynamic bindings for holding the weak pointer. Is there another way to do it? Thank you, Vladimir |
From: Daniel H. <dhe...@te...> - 2010-10-29 05:51:39
|
On Thu, 28 Oct 2010, Vladimir Sedach wrote: > I can't use dynamic bindings for holding the weak pointer. Is there > another way to do it? Yes and no. I don't believe weak pointers and finalizer hooks into garbage collection are the answer[1]. I do believe CL needs some sort of "value semantics" protocol. Much as we like to scoff at languages like C and C++ for not using a garbage collector, they have developed some good protocols to work around this deficiency, in particular "RAII"[2]. Calling constructor and destructor functions as variables go in and out of scope can be used to manage resources, including garbage memory. See for example linear type systems[3]. The reference-counted "smart pointer"[4] can then leverage these scoping rules to provide somewhat inefficient but precise garbage collection for values that need to outlast a lexical scope. The imprecise nature of garbage collection is one reason that educated C++ programmers don't like it[5] -- stuff other than memory needs to be reclaimed. The "GC" we talk about only measures memory usage. It doesn't know that objects such as mutexes or file descriptors have a large cost not reflected in their memory usage. The with-X series of macros address reclamation of non-memory resources having lexical scope; but we don't seem to have a good answer for resources with dynamic scope. Functions participating in a protocol can be instrumented appropriately; but CL doesn't have a universal construct/destroy protocol like C++; thus solutions aren't portable outside of an actively participating codebase. Any hints on how to achieve value semantics for arbitrary objects in CL would be appreciated. Later, Daniel [1] Slightly OT: Java's finalizers aren't even guaranteed to be called during program termination. I don't think SBCL should be held to a higher standard. That CCL's finalizers are being called is a happy implementation detail. Unless the GC really needs to scavenge space, it has no reason to call the finalizer ever. [2] http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization [3] http://en.wikipedia.org/wiki/Linear_type_system [4] http://en.wikipedia.org/wiki/Smart_pointer http://beta.boost.org/doc/libs/1_42_0/libs/smart_ptr/shared_ptr.htm [5] See e.g. http://thread.gmane.org/gmane.comp.lib.boost.devel/188522/focus=188587 ISTR a thread where Dave even explains the C++ committe dismissing GC for similar reasons. |
From: Nikodemus S. <nik...@ra...> - 2010-10-29 07:17:06
|
On 29 October 2010 06:36, Vladimir Sedach <vs...@gm...> wrote: Wait, are you saying that (make-thread (lambda () (catch 'done (finalize <obj> (let ((thread *current-thread*)) (lambda () (sb-thread:interrupt-thread thread (lambda () (throw 'done <computation>)))))) (wait-forever)))) is the exact pattern you are using in your code? That you want to delay a computation to be done by a thread till an object dies? There are couple of issues with this. Firstly, yes, you're going to have a very hard time making sure you don't have life references to <OBJ> till that LAMBDA returns. If you're really want to try it: * (DEBUG 0) is going to be your friend (high debug settings increase variable lifetimes). * You want to structure the code so that WAIT-FOREVER can be in a tail-position, which it isn't here -- CATCH prevents that (as would using BLOCK/RETURN-FROM and closing over the block.) Instead of BLOCK/RETURN or CATCH/THROW you want WAIT-FOREVER to actually return the value. (INTERRUPT-THREAD in production code is also always a bit icky, IMO -- especially forcing a non-local exit using it.) Something along the lines of (make-thread (lambda () (declare (optimize (debug 0))) (let ((sem (sb-thread:make-semaphore))) (finalize obj (lambda () (sb-thread:signal-semaphore sem))) (wait-and-compute sem function)))) (defun wait-and-compute (sem function) (sb-thread:wait-on-semaphore sem) (funcall function)) might have a chance. If it still doesn't fire, you need to insert a bit of frame-scrubbing between the FINALIZE and WAIT-AND-COMPUTE calls. I think it should not be too hard to write a VOP %NUKE-FRAME-REFS that takes a specific object and removes references to it from the frame -- but it doesn't quite fit into this metaphorical margin. Secondly, while a thread is cheap in comparison to a process, it's still a pretty damn big object. Doing this is a rather expensive way to defer computation. Cheers, -- Nikodemus |
From: Vladimir S. <vs...@gm...> - 2010-11-08 00:04:42
|
Looking at this some more, there's two places where a reference to the object to be finalized is returned: sb-ext:weak-pointer-value, and subsequently by sb-ext:finalize. I thought something like this would do the trick: (declaim (notinline finalize-our-thread)) (defun finalize-our-thread (ptr thread) (sb-ext:finalize (sb-ext:weak-pointer-value ptr) (lambda () (sb-thread:interrupt-thread thread (lambda () (throw 'done nil))))) nil) (defun weak-interrupt (ptr) (sb-thread:make-thread (lambda () (finalize-our-thread ptr sb-thread:*current-thread*) (catch 'done (sleep 1000000))))) (defvar thread8 (weak-interrupt (make-weak-pointer (make-instance 'foo)))) (sb-ext:gc :full t) (The goal here is to interrupt the sleeping thread btw, I probably explained that poorly). So the idea is that finalize-our-thread gets called, cleans up the stack after itself, and returns. I'm making three assumptions here - finalize-our-thread cleans up after itself on the stack, doesn't leave a pointer in a register, and doesn't keep intermediate values anywhere else but the stack or the registers. But it seems at least one of those assumptions is wrong, as the object isn't being collected while the thread is sleeping. Another thing is I'm on x86-32, so the conservative collector might be at fault. Would it help if I post the output of disassemble? (I can kind of read it but not understand what's going on) Thank you, Vladimir 2010/10/29 Nikodemus Siivola <nik...@ra...>: > On 29 October 2010 06:36, Vladimir Sedach <vs...@gm...> wrote: > > Wait, are you saying that > > (make-thread > (lambda () > (catch 'done > (finalize <obj> > (let ((thread *current-thread*)) > (lambda () > (sb-thread:interrupt-thread > thread (lambda () (throw 'done <computation>)))))) > (wait-forever)))) > > is the exact pattern you are using in your code? That you want to > delay a computation to be done by a thread till an object dies? > > There are couple of issues with this. > > Firstly, yes, you're going to have a very hard time making sure you > don't have life references to <OBJ> till that LAMBDA returns. > > If you're really want to try it: > > * (DEBUG 0) is going to be your friend (high debug settings increase > variable lifetimes). > > * You want to structure the code so that WAIT-FOREVER can be in a > tail-position, which it isn't here -- CATCH prevents that (as would > using BLOCK/RETURN-FROM and closing over the block.) Instead of > BLOCK/RETURN or CATCH/THROW you want WAIT-FOREVER to actually return > the value. (INTERRUPT-THREAD in production code is also always a bit > icky, IMO -- especially forcing a non-local exit using it.) Something > along the lines of > > (make-thread > (lambda () > (declare (optimize (debug 0))) > (let ((sem (sb-thread:make-semaphore))) > (finalize obj (lambda () (sb-thread:signal-semaphore sem))) > (wait-and-compute sem function)))) > > (defun wait-and-compute (sem function) > (sb-thread:wait-on-semaphore sem) > (funcall function)) > > might have a chance. If it still doesn't fire, you need to insert a > bit of frame-scrubbing between the FINALIZE and WAIT-AND-COMPUTE > calls. I think it should not be too hard to write a VOP > %NUKE-FRAME-REFS that takes a specific object and removes references > to it from the frame -- but it doesn't quite fit into this > metaphorical margin. > > Secondly, while a thread is cheap in comparison to a process, it's > still a pretty damn big object. Doing this is a rather expensive way > to defer computation. > > Cheers, > > -- Nikodemus > |