mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Ractor: the dying thread collects its own objspace post-mortem
The retire GC at notify_exit could not reclaim the terminating thread's scaffolding -- the Thread and Fiber wrappers, the ThreadGroup, the Ractor-local stdio, whatever the machine stack still references -- because at that point they were all still rooted. Those ~11 objects kept ~2 heap pages alive per dead Ractor, merged into the inheritor to wait for its next major GC, so both a spawn-and-join loop and a fire-and-forget loop grew the heap by two pages per Ractor with nothing reclaiming them. Instead, run the final collection at the tail of thread_start_func_2, just before the scheduler handoff. The thread still holds the GVL, so the collection runs lock-free like any local GC (a concurrent global GC waits for its safepoint), and vm_remove_ractor keeps its light lock-only section. r->postmortem makes rb_ractor_mark_local_roots root only what must survive the death -- the join value (r->sync.legacy) and the rb_gc_register_mark_object pins -- and the zombie_objspaces entry recorded later measures exactly the pages the joiner will inherit. While the Ractor is still in vm->ractor.set, rb_vm_mark and ractor_mark keep the Ractor object itself and its default port alive as before; everything they mark is foreign to or pinned by this collection. Two of the swept wrappers are the dying thread's own: its Thread (the rest of the collection resolves GET_EC through ec->thread_ptr) and its root Fiber (whose struct embeds that ec). thread_free and fiber_free recognize "the struct I am standing on" (ec == the current ec), keep the struct, and leave an in-band mark (th->self = 0; cont.self = 0, which already means "no wrapper" on the never-wrapped path). Found as a 1/30 heap corruption, pinned by ASAN. The collecting thread captures those two pointers right after the collection, while its GVL still excludes any global GC, and frees exactly the captured structs at its very last step -- after vm_remove_ractor unlinks the Ractor, anything it did not sweep may already be freed by a concurrent global GC, so the epilogue reads no shared memory at all. Those frees would resolve their objspace through the TLS ec -- which sits inside the fiber struct being freed and leads to the retired Ractor -- so rb_ractor_postmortem_free severs the TLS ec first (the MN epilogue already had) and they fall back to the main objspace. While a thread-event hook is registered the postmortem flag stays off and the collection keeps the ordinary retire roots: callbacks may retain any Ractor's Thread object (rb_internal_thread_event_hook_t), and such references are invisible to the reduced roots. The check takes the hook rwlock, so it happens-after any completed registration; a hook registered after it gets no event from this thread -- rb_thread_execute_hooks skips a thread whose wrapper is gone (th->self == 0) -- so it never receives what the collection swept. On platforms without hook support (win32, none) the check reports false. The collection runs after thread_cleanup_func, so the dying thread's machine stack is already torn down: the root scan skips its machine context (rb_gc_impl_during_postmortem_p), which holds nothing the reduced roots do not already cover and faults when scanned on some platforms (windows-arm). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
207e52a8bf
commit
e768a87a17
17 files changed
+195
-10
Notes:
git
2026-08-10 18:34:21 +00:00
No files matched your search
@@ -1300,6 +1300,22 @@ fiber_mark(void *ptr)
|
||||
|
||||
static void
|
||||
fiber_free(void *ptr)
|
||||
{
|
||||
rb_fiber_t *fiber = ptr;
|
||||
|
||||
/* Root fiber of the thread running the final self collection: saved_ec is the ec
|
||||
* that thread still executes on, so the thread frees the struct itself at its
|
||||
* last step (rb_ractor_postmortem_free). cont.self == 0 already means "no
|
||||
* wrapper" (rb_threadptr_root_fiber_release). */
|
||||
if (&fiber->cont.saved_ec == rb_current_execution_context(false)) {
|
||||
fiber->cont.self = 0;
|
||||
return;
|
||||
}
|
||||
rb_fiber_free_body(ptr);
|
||||
}
|
||||
|
||||
void
|
||||
rb_fiber_free_body(void *ptr)
|
||||
{
|
||||
rb_fiber_t *fiber = ptr;
|
||||
RUBY_FREE_ENTER("fiber");
|
||||
|
||||
@@ -644,6 +644,7 @@ typedef struct gc_function_map {
|
||||
bool (*user_gc_disabled_p)(void *objspace_ptr);
|
||||
bool (*multi_objspace_p)(void);
|
||||
bool (*during_global_gc_p)(void *objspace_ptr);
|
||||
bool (*during_postmortem_p)(void *objspace_ptr);
|
||||
bool (*obj_foreign_p)(void *objspace_ptr, VALUE obj);
|
||||
bool (*shref_marked_p)(void *objspace_ptr, VALUE obj);
|
||||
size_t (*heap_page_count)(void *objspace_ptr);
|
||||
@@ -838,6 +839,7 @@ ruby_modular_gc_init(void)
|
||||
load_modular_gc_func(user_gc_disabled_p);
|
||||
load_modular_gc_func(multi_objspace_p);
|
||||
load_modular_gc_func(during_global_gc_p);
|
||||
load_modular_gc_func(during_postmortem_p);
|
||||
load_modular_gc_func(obj_foreign_p);
|
||||
load_modular_gc_func(shref_marked_p);
|
||||
load_modular_gc_func(heap_page_count);
|
||||
@@ -941,6 +943,7 @@ ruby_modular_gc_init(void)
|
||||
# define rb_gc_impl_user_gc_disabled_p rb_gc_functions.user_gc_disabled_p
|
||||
# define rb_gc_impl_multi_objspace_p rb_gc_functions.multi_objspace_p
|
||||
# define rb_gc_impl_during_global_gc_p rb_gc_functions.during_global_gc_p
|
||||
# define rb_gc_impl_during_postmortem_p rb_gc_functions.during_postmortem_p
|
||||
# define rb_gc_impl_obj_foreign_p rb_gc_functions.obj_foreign_p
|
||||
# define rb_gc_impl_shref_marked_p rb_gc_functions.shref_marked_p
|
||||
# define rb_gc_impl_heap_page_count rb_gc_functions.heap_page_count
|
||||
@@ -3375,8 +3378,14 @@ rb_gc_mark_roots(void *objspace, const char **categoryp)
|
||||
rb_sym_global_symbols_mark_and_move();
|
||||
}
|
||||
|
||||
MARK_CHECKPOINT("machine_context");
|
||||
mark_current_machine_context(ec);
|
||||
/* The dying thread's final collection of its own objspace runs after its stack
|
||||
* has been torn down (thread_cleanup_func), so there is no live machine context
|
||||
* to scan -- the join value and the pins are rooted explicitly. Scanning the
|
||||
* half-dead stack is not only useless but faults on some platforms. */
|
||||
if (!rb_gc_impl_during_postmortem_p(objspace)) {
|
||||
MARK_CHECKPOINT("machine_context");
|
||||
mark_current_machine_context(ec);
|
||||
}
|
||||
|
||||
MARK_CHECKPOINT("finish");
|
||||
|
||||
@@ -4216,6 +4225,17 @@ objspace_absorb_merge(void *dst, void *src)
|
||||
gc_absorbed_since_global_gc = true;
|
||||
}
|
||||
|
||||
/* The dying thread's last collection of its own objspace, GVL still held; with
|
||||
* r->postmortem set, rb_ractor_mark_local_roots roots only the join value and the
|
||||
* registered_marks pins, so the scaffolding nobody needs any more dies here. */
|
||||
void
|
||||
rb_gc_objspace_postmortem_self(void)
|
||||
{
|
||||
if (!rb_gc_impl_multi_objspace_p()) return;
|
||||
|
||||
rb_gc_impl_objspace_retire_gc(rb_gc_get_objspace());
|
||||
}
|
||||
|
||||
void
|
||||
rb_gc_objspace_absorb_into_current(void **objspace_slot)
|
||||
{
|
||||
|
||||
@@ -605,6 +605,7 @@ typedef struct rb_objspace {
|
||||
unsigned int during_reference_updating : 1;
|
||||
unsigned int during_minor_gc : 1;
|
||||
unsigned int during_incremental_marking : 1;
|
||||
unsigned int during_postmortem : 1;
|
||||
unsigned int measure_gc : 1;
|
||||
} flags;
|
||||
|
||||
@@ -7805,12 +7806,25 @@ rb_gc_impl_objspace_retire_gc(void *objspace_ptr)
|
||||
{
|
||||
rb_objspace_t *objspace = objspace_ptr;
|
||||
|
||||
/* The dying thread's stack is already torn down here, so the root scan must skip
|
||||
* its machine context (rb_gc_mark_roots). */
|
||||
objspace->flags.during_postmortem = 1;
|
||||
|
||||
gc_rest(objspace);
|
||||
gc_start_body(objspace, GPR_FLAG_FULL_MARK | GPR_FLAG_IMMEDIATE_MARK | GPR_FLAG_IMMEDIATE_SWEEP,
|
||||
false);
|
||||
|
||||
heap_pages_freeable_pages = objspace->empty_pages_count;
|
||||
heap_pages_free_unused_pages(objspace);
|
||||
|
||||
objspace->flags.during_postmortem = 0;
|
||||
}
|
||||
|
||||
bool
|
||||
rb_gc_impl_during_postmortem_p(void *objspace_ptr)
|
||||
{
|
||||
rb_objspace_t *objspace = objspace_ptr;
|
||||
return objspace->flags.during_postmortem != 0;
|
||||
}
|
||||
|
||||
static void
|
||||
|
||||
@@ -137,6 +137,9 @@ GC_IMPL_FN void rb_gc_impl_each_objects_foreign(void *objspace_ptr, int (*callba
|
||||
* objspace and passes the per-Ractor objspace machinery (retire, absorb, ...) straight through. */
|
||||
GC_IMPL_FN bool rb_gc_impl_multi_objspace_p(void);
|
||||
GC_IMPL_FN bool rb_gc_impl_during_global_gc_p(void *objspace_ptr);
|
||||
/* Whether the current collection is a dying thread's final collection of its own
|
||||
* objspace, whose torn-down machine context must not be scanned. */
|
||||
GC_IMPL_FN bool rb_gc_impl_during_postmortem_p(void *objspace_ptr);
|
||||
/* Whether obj is owned by an objspace other than objspace_ptr. Always false for a single
|
||||
* objspace impl. */
|
||||
GC_IMPL_FN bool rb_gc_impl_obj_foreign_p(void *objspace_ptr, VALUE obj);
|
||||
|
||||
@@ -1815,6 +1815,13 @@ rb_gc_impl_during_global_gc_p(void *objspace_ptr)
|
||||
return objspace->world_stopped;
|
||||
}
|
||||
|
||||
bool
|
||||
rb_gc_impl_during_postmortem_p(void *objspace_ptr)
|
||||
{
|
||||
/* mmtk has a single objspace and no per-Ractor retire collection. */
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
rb_gc_impl_obj_foreign_p(void *objspace_ptr, VALUE obj)
|
||||
{
|
||||
|
||||
@@ -31,4 +31,5 @@ VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb
|
||||
VALUE rb_fiberptr_self(struct rb_fiber_struct *fiber);
|
||||
unsigned int rb_fiberptr_blocking(struct rb_fiber_struct *fiber);
|
||||
struct rb_execution_context_struct * rb_fiberptr_get_ec(struct rb_fiber_struct *fiber);
|
||||
void rb_fiber_free_body(void *fiber); /* cont.c */
|
||||
#endif /* INTERNAL_CONT_H */
|
||||
@@ -305,6 +305,7 @@ bool rb_gc_obj_foreign_p(VALUE obj);
|
||||
void *rb_gc_objspace_alloc(void);
|
||||
void rb_gc_objspace_retire_gc(void);
|
||||
void rb_gc_objspace_retire(void **objspace_slot);
|
||||
void rb_gc_objspace_postmortem_self(void);
|
||||
void rb_gc_objspace_absorb_into_current(void **objspace_slot);
|
||||
void rb_gc_objspace_absorb_all_zombies(void);
|
||||
void rb_gc_objspace_disown(void *objspace);
|
||||
|
||||
@@ -55,6 +55,9 @@ void rb_ec_check_ints(struct rb_execution_context_struct *ec);
|
||||
|
||||
void rb_thread_free_native_thread(void *th_ptr);
|
||||
|
||||
bool rb_thread_event_hooks_registered_p(void); /* thread_pthread.c */
|
||||
void rb_thread_free_body(void *th); /* vm.c */
|
||||
|
||||
RUBY_SYMBOL_EXPORT_BEGIN
|
||||
|
||||
void *rb_thread_prevent_fork(void *(*func)(void *), void *data); /* for ext/socket/raddrinfo.c */
|
||||
|
||||
@@ -324,6 +324,14 @@ ractor_mark(void *ptr)
|
||||
void
|
||||
rb_ractor_mark_local_roots(rb_ractor_t *r)
|
||||
{
|
||||
if (r->postmortem) {
|
||||
/* The final self collection: everything else -- the Thread and Fiber
|
||||
* wrappers, stdio, stack leftovers -- is what it exists to reclaim. */
|
||||
rb_ractor_mark_terminated_join_value(r);
|
||||
rb_gc_mark_vm_stack_values((long)r->registered_marks_cnt, r->registered_marks);
|
||||
return;
|
||||
}
|
||||
|
||||
rb_gc_mark(r->loc);
|
||||
rb_gc_mark(r->name);
|
||||
ractor_mark_unshareable_parts(r);
|
||||
@@ -621,6 +629,8 @@ vm_remove_ractor(rb_vm_t *vm, rb_ractor_t *cr)
|
||||
* cnt==1-no-zombie window where a GC skips shareable pinning and collects
|
||||
* objects (a cc, say) reachable only through this objspace. */
|
||||
if (cr->objspace) {
|
||||
/* The final self collection already ran (rb_ractor_postmortem_collect),
|
||||
* so the entry measures exactly the pages the joiner will inherit. */
|
||||
rb_gc_objspace_retire(&cr->objspace);
|
||||
}
|
||||
vm->ractor.cnt--;
|
||||
@@ -630,6 +640,50 @@ vm_remove_ractor(rb_vm_t *vm, rb_ractor_t *cr)
|
||||
RB_VM_UNLOCK();
|
||||
}
|
||||
|
||||
/* The dying thread's final collection of its own objspace, and the capture of what it
|
||||
* must free itself. Called with the GVL still held: a concurrent global GC waits for
|
||||
* this thread's safepoint, so the collection is lock-free like any local GC. */
|
||||
void
|
||||
rb_ractor_postmortem_collect(rb_thread_t *th, struct rb_ractor_postmortem_frees *pf)
|
||||
{
|
||||
rb_ractor_t *const cr = th->ractor;
|
||||
VM_ASSERT(cr != GET_VM()->ractor.main_ractor);
|
||||
VM_ASSERT(th->ec != NULL);
|
||||
|
||||
struct rb_fiber_struct *const fiber = th->ec->fiber_ptr;
|
||||
const bool fiber_wrapped = fiber && rb_fiberptr_self(fiber) != 0;
|
||||
|
||||
/* With a thread-event hook registered the callbacks may retain any Ractor's Thread
|
||||
* object (rb_internal_thread_event_hook_t), and such references are invisible to
|
||||
* the reduced roots: keep the ordinary retire roots, wrappers survive to absorb. */
|
||||
cr->postmortem = rb_gc_multi_objspace_p() && !rb_thread_event_hooks_registered_p();
|
||||
rb_gc_objspace_postmortem_self();
|
||||
|
||||
/* Only what this collection provably swept (wrapper gone, struct deferred with an
|
||||
* in-band mark) may be touched after vm_remove_ractor unlinks the Ractor. */
|
||||
pf->th = (th->self == 0) ? th : NULL;
|
||||
pf->fiber = (fiber_wrapped && rb_fiberptr_self(fiber) == 0) ? fiber : NULL;
|
||||
}
|
||||
|
||||
void
|
||||
rb_ractor_postmortem_free(const struct rb_ractor_postmortem_frees *pf)
|
||||
{
|
||||
if (pf->fiber == NULL && pf->th == NULL) return;
|
||||
|
||||
/* The frees below resolve their objspace through the TLS ec, which sits inside
|
||||
* pf->fiber and leads to the retired Ractor: cut the resolution off so they fall
|
||||
* back to the main objspace. (The MN epilogue has already done this.) */
|
||||
#ifdef RB_THREAD_LOCAL_SPECIFIER
|
||||
rb_current_ec_set(NULL);
|
||||
#else
|
||||
native_tls_set(ruby_current_ec_key, NULL);
|
||||
#endif
|
||||
|
||||
/* the fiber struct embeds the thread's final ec, so free it first */
|
||||
if (pf->fiber) rb_fiber_free_body(pf->fiber);
|
||||
if (pf->th) rb_thread_free_body(pf->th);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
ractor_alloc(VALUE klass)
|
||||
{
|
||||
|
||||
@@ -125,6 +125,10 @@ struct rb_ractor_struct {
|
||||
|
||||
struct ccan_list_node vmlr_node;
|
||||
bool in_terminated_set; /* vmlr_node is on vm->ractor.terminated_set */
|
||||
/* The final self collection ran (rb_ractor_postmortem_collect): from then on
|
||||
* rb_ractor_mark_local_roots roots only the join value and the registered_marks
|
||||
* pins, so the dying thread's own scaffolding can be collected. */
|
||||
bool postmortem;
|
||||
|
||||
// ractor local data
|
||||
|
||||
@@ -221,6 +225,15 @@ bool rb_ractor_p(VALUE rv);
|
||||
void rb_ractor_living_threads_init(rb_ractor_t *r);
|
||||
void rb_ractor_living_threads_insert(rb_ractor_t *r, rb_thread_t *th);
|
||||
void rb_ractor_living_threads_remove(rb_ractor_t *r, rb_thread_t *th);
|
||||
|
||||
/* The structs the final self collection swept while the dying thread still stood on
|
||||
* them; that thread frees them at its very last step (rb_ractor_postmortem_free). */
|
||||
struct rb_ractor_postmortem_frees {
|
||||
struct rb_thread_struct *th;
|
||||
struct rb_fiber_struct *fiber;
|
||||
};
|
||||
void rb_ractor_postmortem_collect(rb_thread_t *th, struct rb_ractor_postmortem_frees *pf);
|
||||
void rb_ractor_postmortem_free(const struct rb_ractor_postmortem_frees *pf);
|
||||
void rb_ractor_cancel_creation(rb_ractor_t *r, rb_thread_t *th);
|
||||
void rb_ractor_blocking_threads_inc(rb_ractor_t *r, const char *file, int line); // TODO: file, line only for RUBY_DEBUG_LOG
|
||||
void rb_ractor_blocking_threads_dec(rb_ractor_t *r, const char *file, int line); // TODO: file, line only for RUBY_DEBUG_LOG
|
||||
|
||||
@@ -675,12 +675,6 @@ ractor_notify_exit(rb_execution_context_t *ec, rb_ractor_t *cr, VALUE legacy, bo
|
||||
VM_ASSERT(!UNDEF_P(legacy));
|
||||
VM_ASSERT(cr->sync.legacy == Qundef);
|
||||
|
||||
/* Last local GC before termination, in ordinary execution context: collect here
|
||||
* what the joiner would otherwise inherit, and return empty pages to the pool. */
|
||||
if (cr != GET_VM()->ractor.main_ractor) {
|
||||
rb_gc_objspace_retire_gc();
|
||||
}
|
||||
|
||||
RACTOR_LOCK_SELF(cr);
|
||||
{
|
||||
ractor_free_all_ports(cr);
|
||||
|
||||
@@ -827,12 +827,20 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start)
|
||||
thread_cleanup_func(th, FALSE);
|
||||
VM_ASSERT(th->ec->vm_stack == NULL);
|
||||
|
||||
// A dying Ractor collects its own objspace here, before the scheduler handoff
|
||||
// below, and captures the structs it must free at its last step.
|
||||
struct rb_ractor_postmortem_frees pf = { NULL, NULL };
|
||||
if (th->invoke_type == thread_invoke_type_ractor_proc) {
|
||||
rb_ractor_postmortem_collect(th, &pf);
|
||||
}
|
||||
|
||||
#if defined(USE_MN_THREADS) && USE_MN_THREADS
|
||||
if (th_has_coroutine(th)) {
|
||||
// Run the coroutine thread's epilogue here, while th is still valid;
|
||||
// co_start then only makes the final transfer (see
|
||||
// coroutine_thread_terminated in thread_pthread_mn.c).
|
||||
coroutine_thread_terminated(th);
|
||||
rb_ractor_postmortem_free(&pf);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -843,6 +851,7 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start)
|
||||
// So gvl_release() should be before it.
|
||||
thread_sched_to_dead(TH_SCHED(th), th);
|
||||
rb_ractor_living_threads_remove(th->ractor, th);
|
||||
rb_ractor_postmortem_free(&pf);
|
||||
}
|
||||
else {
|
||||
rb_ractor_living_threads_remove(th->ractor, th);
|
||||
|
||||
@@ -331,6 +331,12 @@ rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)
|
||||
// no-op
|
||||
}
|
||||
|
||||
bool
|
||||
rb_thread_event_hooks_registered_p(void)
|
||||
{
|
||||
return false; // hooks are not implemented on this platform
|
||||
}
|
||||
|
||||
#endif /* THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION */
|
||||
|
||||
void
|
||||
|
||||
@@ -3558,6 +3558,24 @@ struct rb_internal_thread_event_hook {
|
||||
|
||||
static pthread_rwlock_t rb_internal_thread_event_hooks_rw_lock = PTHREAD_RWLOCK_INITIALIZER;
|
||||
|
||||
/* For the GC: whether any thread-event hook is registered right now. The rwlock
|
||||
* makes the answer happen-after any completed registration; a hook registered
|
||||
* after this read gets no event from the asking thread (rb_thread_execute_hooks
|
||||
* skips a swept thread), so it never sees the objects the caller may collect. */
|
||||
bool
|
||||
rb_thread_event_hooks_registered_p(void)
|
||||
{
|
||||
int r;
|
||||
if ((r = pthread_rwlock_rdlock(&rb_internal_thread_event_hooks_rw_lock))) {
|
||||
rb_bug_errno("pthread_rwlock_rdlock", r);
|
||||
}
|
||||
const bool registered = (rb_internal_thread_event_hooks != NULL);
|
||||
if ((r = pthread_rwlock_unlock(&rb_internal_thread_event_hooks_rw_lock))) {
|
||||
rb_bug_errno("pthread_rwlock_unlock", r);
|
||||
}
|
||||
return registered;
|
||||
}
|
||||
|
||||
#if defined(HAVE_WORKING_FORK)
|
||||
static void
|
||||
rb_internal_thread_event_hooks_rw_lock_atfork(void)
|
||||
@@ -3636,6 +3654,10 @@ static void
|
||||
rb_thread_execute_hooks(rb_event_flag_t event, rb_thread_t *th)
|
||||
{
|
||||
int r;
|
||||
|
||||
/* th->self == 0: the dying thread's final collection swept its Thread wrapper, so
|
||||
* no hook existed then; one registered since has no ordering claim to this event. */
|
||||
if (th->self == 0) return;
|
||||
if ((r = pthread_rwlock_rdlock(&rb_internal_thread_event_hooks_rw_lock))) {
|
||||
rb_bug_errno("pthread_rwlock_rdlock", r);
|
||||
}
|
||||
|
||||
+3
-2
@@ -603,8 +603,9 @@ native_thread_create_shared(rb_thread_t *th)
|
||||
RUBY_DEBUG_LOG("th:%u vm_stack:%p machine_stack:%p", rb_th_serial(th), vm_stack, machine_stack);
|
||||
thread_sched_to_ready(TH_SCHED(th), th);
|
||||
|
||||
// setup nt
|
||||
return native_thread_check_and_create_shared(th->vm);
|
||||
// setup nt. th is runnable now and a Ractor's thread that runs to its end frees
|
||||
// its own rb_thread_t (rb_ractor_postmortem_free), so th must not be read again.
|
||||
return native_thread_check_and_create_shared(vm);
|
||||
}
|
||||
|
||||
#else // USE_MN_THREADS
|
||||
|
||||
@@ -44,6 +44,12 @@ rb_internal_thread_remove_event_hook(rb_internal_thread_event_hook_t * hook)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
rb_thread_event_hooks_registered_p(void)
|
||||
{
|
||||
return false; // hooks are not implemented on this platform
|
||||
}
|
||||
|
||||
RBIMPL_ATTR_NORETURN()
|
||||
static void
|
||||
w32_error(const char *func)
|
||||
|
||||
@@ -3992,6 +3992,21 @@ void rb_threadptr_sched_free(rb_thread_t *th); // thread_*.c
|
||||
|
||||
static void
|
||||
thread_free(void *ptr)
|
||||
{
|
||||
rb_thread_t *th = ptr;
|
||||
|
||||
/* The final self collection sweeps the wrapper of the very thread running it; the
|
||||
* struct is still under that thread's feet (GET_EC resolves through it), so the
|
||||
* thread frees the struct itself at its last step (rb_ractor_postmortem_free). */
|
||||
if (th->ec != NULL && th->ec == rb_current_execution_context(false)) {
|
||||
th->self = 0;
|
||||
return;
|
||||
}
|
||||
rb_thread_free_body(ptr);
|
||||
}
|
||||
|
||||
void
|
||||
rb_thread_free_body(void *ptr)
|
||||
{
|
||||
rb_thread_t *th = ptr;
|
||||
RUBY_FREE_ENTER("thread");
|
||||
|
||||
Reference in new issue
Block a user