Hacker News new | ask | show | jobs
by kazinator 3038 days ago
By your reasoning, everything is manually allocated. When we call (cons 1 2) in Lisp, that's a manually allocated cons. Most objects come into life due to some "manual" construction in the program!

> The thread must die by explicit programmatic action, which in turn will free its allocated block of stack memory.

Simply, no. A thread can recurse through some function activations and then hit an exit statement which terminates it without destroying those activations. Other threads can have pointers into that thread's stack, so the stack must not be reclaimed until they let go.

For instance, a tread allocates a reply box on the stack and calls some message passing API to send a message, whereby it registers the reply box. That API now has a pointer into the thread's stack. Suppose the thread dies before unregistering the reply box. If that can happen, the stack has to stick around. When the API tries to reply to the thread, and finds that the thread is dead, it can dequeue the reply box at that time; then the stack becomes unreachable. If the stack is reclaimed before then, then this messaging API will be traversing bad pointers through this registered message box.

> because other resources almost certainly have no necessary correlation with memory pressure

This is true and there is a way to regard finalization as decoupled from GC.

I have experience implementing an object system in which finalization is treated similarly to C++ destructors.

Objects can expect to have their finalizers explicitly invoked even when they are still live, long before they have become garbage.

One situation in which that happens is if an exception is thrown during the construction of an object.

I also have scoped reclamation construct with-objects which implements RAII. For instance:

    (with-objects ((foo (new foo ...))
                   (bar (new bar ...))
       ...)
The finalizers of foo and bar are invoked when with-objects terminates. Additionally, if foo throws during its construction, its finalizer is called, and if bar throws during its construction, both are called.

Now if those finalizers are not invoked by the time those objects become garbage, then GC will invoke them. Basically a finalizer is invoked and then removed, so if called early, then GC doesn't call it any more.

Situations in which it's undesirable or infeasible to use with-objects are covered by the call-finalizers API: a one-argument function which takes an object, and calls and removes its finalizer, allowing for completely manual finalization.

Of course, this is basically a logical extension of how with-open-file works in Common Lisp, formalized into the object system, integrated with finalizers.

There is a tradeoff: timely reclamation versus the risk created by the possibility of premature reclamation. It's easy to make objects which can be safely used after their finalizer has been called; the risk isn't that of a dangling pointer that will blow up the show. Still, things can malfunction when objects "hollowed out" by finalization are relied upon to still be functional.

1 comments

> Other threads can have pointers into that thread's stack, so the stack must not be reclaimed until they let go.

> For instance, a tread allocates a reply box on the stack and calls some message passing API to send a message, whereby it registers the reply box. That API now has a pointer into the thread's stack. Suppose the thread dies before unregistering the reply box. If that can happen, the stack has to stick around.

OMG, can such thing really happen and be correct in some language? The thing is, the stack is most often used as method-local storage for that method's variables and objects that are not escaping its scope and thus can be allocated on stack instead of the heap.

Basically, what happens with stack in a language like C or Java when in thread T some method A calls another method B is the following:

  T.stack.push(return address)
  T.stack.allocateFrame(B)

Then, when B has done everything it wanted to, it clears its frame from the stack and returns by saved address into A. Similarly, when an exception is thrown, it crawls up the stack frame-by-frame clearing them out until it finds the handler.

With that general scheme in mind, I see some contradictions in your example:

1. How exactly can a thread correctly die in such a way that frame of method that allocated the reply box on-stack is not cleaned up from it first?

2. Why the method even allocated shared data structure on its own stack (which it must clear on returning to caller) and not in heap?

3. If it did so because it blocks while waiting for the response to appear in stack-allocated placeholder, then why would anyone consider thread which is actively waiting for something dead and try to reclaim its stack?

> How exactly can a thread correctly die in such a way that frame of method that allocated the reply box on-stack is not cleaned up from it first?

The thread could die incorrectly in that way.

> Why the method even allocated shared data structure on its own stack

Done for efficiency or when it's not desirable to have to check and recover from a failure to allocate such a structure. E.g. DECLARE_WAITQUEUE macro in Linux kernel:

https://elixir.bootlin.com/linux/v4.3/source/include/linux/w...

How blocking is implemented in Linux is that tasks declare wait queue nodes on the stack, register these into a queue, then change themselves to a sleep state and call the scheduler.

> why would anyone consider thread which is actively waiting for something dead and try to reclaim its stack?

What reclaims the stack isn't that "anyone"; it's garbage collection. It's plausible like this. Suppose the messaging API is responsible for dequeuing the waiter. The messaging API walks the queue, depositing replies into the reply boxes and dequeueing (without caring whether the associated threads are alive or dead).

When it dequeues the dead thread's reply box, that stack then becomes unreachable. Now it is eligible for reclamation.