Threads and parking
Threads and parking
A collection needs every thread stopped at a known point. A thread blocked inside a syscall cannot reach one, so it must declare itself parked first.
The rule
Every blocking call made from an attached thread must be bracketed.
pgc_begin_blocking();
result = recv(fd, buffer, len, 0);
pgc_end_blocking();Or with the macro:
PGC_BLOCKING(result = recv(fd, buffer, len, 0));That covers every mutex acquisition, condition wait, socket read or write, accept, sleep, and process wait.
pgc_begin_blocking says "I hold no live managed pointers I will touch while
blocked, so scan my last safepoint and carry on." pgc_end_blocking waits out any
collection in progress before the thread resumes.
What happens without it
The collector loops waiting for every thread to park. A thread sitting in recv
never parks. The entire process hangs on the next allocation that triggers a
collection, with no diagnostic. It looks like a deadlock, because it is one.
This is the single most common way to break a working program with a small change, and it is worth checking any time a new blocking call appears in C.
Attaching
A thread that touches managed memory must attach before its first managed access and detach when done, from inside itself, near the top of its entry function:
static void *worker(void *arg) {
pgc_thread_attach();
/* ... managed work ... */
pgc_thread_detach();
return NULL;
}Attaching records the thread so the collector can stop it and scan its stack for roots. Detaching matters as much: a thread that never detaches leaves a permanently reserved region that the collector cannot compact past.
If you spawn threads through std::threads, all of this is handled for you.
The ordering rules
Two constraints that are easy to violate and hard to diagnose.
No GC operation while parked. Creating a handle, allocating, or touching
managed memory between pgc_begin_blocking and pgc_end_blocking is invalid.
Prepare first, then park:
pgc_handle h = pgc_handle_create(item); /* before parking */
pgc_begin_blocking();
peko_mutex_lock(&lock);
pgc_end_blocking(); /* unpark while holding the lock */No GC operation while holding a lock. Another thread may be parked waiting on that lock, so a collection triggered under it cannot complete.
Note the idiom above: park to acquire, then unpark immediately, so the thread runs unparked while it holds the lock and does managed work.
Callbacks from a parked loop
A native event loop is normally parked for its whole run. When it calls back into PekoScript, that callback will allocate, which a parked thread must not do. So the trampoline unparks around the call and re-resolves the closure through its handle:
static void trampoline(const char *req, void *arg) {
binding *b = (binding *)arg;
pgc_end_blocking();
void *ctx = pgc_handle_get(b->ctx); /* may have moved */
char *result = b->fn(ctx, req);
pgc_begin_blocking();
}That shape is the template for any C library that calls you back from a blocking loop.