mirror of
https://github.com/ruby/ruby.git
synced 2026-07-31 09:25:15 +08:00
Redesign the teardown of M:N (coroutine) threads around the natural call structure nt_start -> co_start -> thread_start_func_2: when the thread body returns, its epilogue (coroutine_thread_terminated) finishes all rb_thread_t / rb_ractor_t business while th is still valid, and co_start then makes exactly one final transfer back to the native thread's own context (nt_start's loop), where the nt reclaims the dead coroutine's context. The resume itself proves the final transfer's register save completed, so the reclaim needs no list, no handshake and no extra locking. This deletes the zombie-threads machinery (VM-global list, sched.finished publish, the GC mark/reap pass) and closes its UAF window: publishing `finished` before the terminal transfer let the GC free the Thread wrapper -- whose coroutine context the transfer still saves into -- mid-teardown. * struct rb_thread_context bundles a coroutine thread's execution-owned resources: the coroutine_context (kept first, so th->sched.context points into the block) plus its machine stack, the stashed final-transfer target (nt), and a dead flag. co_start's epilogue sets the flag and transfers via the stashed nt, touching tctx alone -- th may already be collected by then. The nt loop passes every coroutine it resumes from to thread_sched_reclaim_from(), which frees dead ones (stack back to the pool). A live coroutine cannot be resumed elsewhere, die and be freed while the nt loop still inspects it: a live yield arrives with the sched lock handed off, and no other nt can schedule that thread again until the nt loop's own unlock. * coroutine_thread_terminated (run from thread_start_func_2, where the upstream non-coroutine teardown also runs, while th is still valid and listed) does: wake the next thread as upstream's thread_sched_to_dead does; stash th->nt into tctx and detach th->sched.context; leave the living set; clear the current ec; and, for a shared-nt non-last thread, enqueue the Ractor's next runnable thread (not earlier: waking it from to_dead would let another thread of the same Ractor execute while this teardown still runs). Everything that reads th happens before the living-set removal, after which the GC may collect th (and, for a Ractor's main thread, the Ractor). * interrupt_lock is now destroyed in thread_free instead of during teardown (thread_cleanup_func): while th is in its Ractor's living set, anyone -- terminate_all on the Ractor's terminating main thread, Thread#kill / #raise -- may lock it, and destroying it mid-teardown leaves a window where a concurrent interrupter locks a destroyed mutex (EINVAL; the upstream non-coroutine path has the same, smaller, window). At thread_free time the wrapper is unreachable AND off the living set (listed threads are marked), so no interrupter can exist. The atfork cleanup reinitializes the copied lock, which may have been held at the instant of fork. * The Thread wrapper's dfree only reclaims the context of a thread that never started; a terminated one cleared th->sched.context in its epilogue. VM shutdown needs no extra synchronization: past the living-set removal the epilogue touches only its own context block, the TLS and nt->nt_context, none of which VM destruct frees (SNT native threads and the machine-stack pool are intentionally not torn down at exit, as before). Verified: bootstraptest 2050 PASS; test_thread / test_ractor / test_fiber / test_gc / test_gc_compact / thread_queue 282 tests 0F0E; Ractor x Thread x IO teardown churn 150/150 + 60/60 (exercises DNT promotion, terminate_all racing dying threads, and the double-enqueue fixed in the previous commit); fork with live Ractors + child GC 20/20; ASAN clean including unreferenced Thread/Ractor collection (the old zombie window) and mixed stress; RGENGC_CHECK_MODE=2 btest 2050 + churn; TSan warning profile identical to master (the new functions appear in no race report); YJIT move-under-GC.stress; kill/join races; Thread object retention across Ractor death; exit-while-winding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
343 lines
6.0 KiB
C
343 lines
6.0 KiB
C
/*
|
|
A thread interface implementation without any system thread.
|
|
|
|
Assumption:
|
|
* There is a only single thread in the ruby process
|
|
* No signal happens targeting the ruby process
|
|
|
|
Note:
|
|
* No thread switching in the VM
|
|
* No timer thread because thread switching won't happen
|
|
* No mutex guard because the VM won't be racy
|
|
*/
|
|
|
|
#ifdef THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION
|
|
|
|
#include <time.h>
|
|
|
|
#if defined(__wasm__) && !defined(__EMSCRIPTEN__)
|
|
# include "wasm/machine.h"
|
|
#endif
|
|
|
|
// Do nothing for GVL
|
|
static void
|
|
thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th)
|
|
{
|
|
}
|
|
|
|
static void
|
|
thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately)
|
|
{
|
|
}
|
|
|
|
#define thread_sched_to_dead(a,b) thread_sched_to_waiting(a,b,true)
|
|
|
|
static void
|
|
thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
|
|
{
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
rb_thread_sched_destroy(struct rb_thread_sched *sched)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
// Do nothing for mutex guard
|
|
void
|
|
rb_native_mutex_lock(rb_nativethread_lock_t *lock)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_mutex_unlock(rb_nativethread_lock_t *lock)
|
|
{
|
|
}
|
|
|
|
int
|
|
rb_native_mutex_trylock(rb_nativethread_lock_t *lock)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
rb_native_mutex_initialize(rb_nativethread_lock_t *lock)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_mutex_destroy(rb_nativethread_lock_t *lock)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_cond_initialize(rb_nativethread_cond_t *cond)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_cond_destroy(rb_nativethread_cond_t *cond)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_cond_signal(rb_nativethread_cond_t *cond)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_cond_broadcast(rb_nativethread_cond_t *cond)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_cond_wait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_native_cond_timedwait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex, unsigned long msec)
|
|
{
|
|
}
|
|
|
|
// The only one thread in process
|
|
static rb_thread_t *ruby_native_thread;
|
|
|
|
rb_thread_t *
|
|
ruby_thread_from_native(void)
|
|
{
|
|
return ruby_native_thread;
|
|
}
|
|
|
|
int
|
|
ruby_thread_set_native(rb_thread_t *th)
|
|
{
|
|
if (th && th->ec) {
|
|
rb_ractor_set_current_ec(th->ractor, th->ec);
|
|
}
|
|
ruby_native_thread = th;
|
|
return 1; // always succeed
|
|
}
|
|
|
|
void
|
|
Init_native_thread(rb_thread_t *main_th)
|
|
{
|
|
// no TLS setup and no thread id setup
|
|
ruby_thread_set_native(main_th);
|
|
}
|
|
|
|
void
|
|
ruby_mn_threads_params(void)
|
|
{
|
|
}
|
|
|
|
static void
|
|
native_thread_destroy_atfork(struct rb_native_thread *nt)
|
|
{
|
|
/* no-op */
|
|
}
|
|
|
|
static int
|
|
native_thread_init_stack(rb_thread_t *th, void *local_in_parent_frame)
|
|
{
|
|
#if defined(__wasm__) && !defined(__EMSCRIPTEN__)
|
|
th->ec->machine.stack_start = (VALUE *)rb_wasm_stack_get_base();
|
|
#endif
|
|
return 0; // success
|
|
}
|
|
|
|
static int
|
|
native_thread_create(rb_thread_t *th)
|
|
{
|
|
th->status = THREAD_KILLED;
|
|
rb_ractor_living_threads_remove(th->ractor, th);
|
|
rb_notimplement();
|
|
}
|
|
|
|
// Do nothing for handling ubf because no other thread doesn't exist and unblock anything
|
|
#define register_ubf_list(th) (void)(th)
|
|
#define unregister_ubf_list(th) (void)(th)
|
|
#define ubf_select 0
|
|
|
|
inline static void
|
|
ubf_wakeup_all_threads(void)
|
|
{
|
|
return;
|
|
}
|
|
|
|
inline static int
|
|
ubf_threads_empty(void)
|
|
{
|
|
return 1; // true
|
|
}
|
|
|
|
inline static void
|
|
ubf_list_atfork()
|
|
{
|
|
}
|
|
|
|
inline static void
|
|
ubf_timer_disarm(void)
|
|
{
|
|
}
|
|
|
|
|
|
// No timer thread because thread switching won't happen
|
|
#define TIMER_THREAD_CREATED_P() (1)
|
|
inline static void
|
|
rb_thread_create_timer_thread(void)
|
|
{
|
|
}
|
|
|
|
void
|
|
rb_thread_wakeup_timer_thread(int sig)
|
|
{
|
|
}
|
|
|
|
inline static int
|
|
native_stop_timer_thread(void)
|
|
{
|
|
return 1; // success
|
|
}
|
|
|
|
inline static void
|
|
native_reset_timer_thread(void)
|
|
{
|
|
}
|
|
|
|
// Do nothing for thread naming
|
|
inline static void
|
|
native_set_thread_name(rb_thread_t *th)
|
|
{
|
|
}
|
|
|
|
inline static void
|
|
native_set_another_thread_name(rb_nativethread_id_t thread_id, VALUE name)
|
|
{
|
|
}
|
|
|
|
// Don't expose native thread id for now to keep system's thread API agnostic
|
|
#define USE_NATIVE_THREAD_NATIVE_THREAD_ID 0
|
|
|
|
// No reserved fd for piping threads
|
|
int
|
|
rb_reserved_fd_p(int fd)
|
|
{
|
|
return 0; // not reserved
|
|
}
|
|
|
|
// Don't expose native thread info for now to keep system's thread API agnostic
|
|
rb_nativethread_id_t
|
|
rb_nativethread_self(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
// Do nothing for sigwait things because of no signal assumption
|
|
// Q(katei): is this correct description?
|
|
int
|
|
rb_sigwait_fd_get(const rb_thread_t *th)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
NORETURN(void rb_sigwait_fd_put(rb_thread_t *, int));
|
|
void
|
|
rb_sigwait_fd_put(rb_thread_t *th, int fd)
|
|
{
|
|
rb_bug("not implemented, should not be called rb_sigwait_fd_put");
|
|
}
|
|
|
|
NORETURN(void rb_sigwait_sleep(const rb_thread_t *, int, const rb_hrtime_t *));
|
|
void
|
|
rb_sigwait_sleep(const rb_thread_t *th, int sigwait_fd, const rb_hrtime_t *rel)
|
|
{
|
|
rb_bug("not implemented, should not be called rb_sigwait_sleep");
|
|
}
|
|
|
|
static void
|
|
native_sleep(rb_thread_t *th, rb_hrtime_t *rel)
|
|
{
|
|
// No signal assumption allows the use of uninterruptible sleep
|
|
struct timespec ts;
|
|
(void)clock_nanosleep(CLOCK_REALTIME, 0, rb_hrtime2timespec(&ts, rel), NULL);
|
|
}
|
|
|
|
static int
|
|
native_fd_select(int n, rb_fdset_t *readfds, rb_fdset_t *writefds, rb_fdset_t *exceptfds, struct timeval *timeout, rb_thread_t *th)
|
|
{
|
|
return rb_fd_select(n, readfds, writefds, exceptfds, timeout);
|
|
}
|
|
|
|
static bool
|
|
th_has_dedicated_nt(const rb_thread_t *th)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void
|
|
rb_add_running_thread(rb_thread_t *th)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
void
|
|
rb_del_running_thread(rb_thread_t *th)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
void
|
|
rb_threadptr_sched_free(rb_thread_t *th)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
void
|
|
rb_ractor_sched_barrier_start(rb_vm_t *vm, rb_ractor_t *cr)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
void
|
|
rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
rb_thread_lock_native_thread(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void *
|
|
rb_thread_prevent_fork(void *(*func)(void *), void *data)
|
|
{
|
|
return func(data);
|
|
}
|
|
|
|
void
|
|
rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)
|
|
{
|
|
// no-op
|
|
}
|
|
|
|
#endif /* THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION */
|
|
|
|
void
|
|
rb_thread_sched_wait_winding(rb_vm_t *vm)
|
|
{
|
|
// no coroutine (M:N) threads on this implementation: nothing winds down
|
|
// after leaving the living set (see thread_pthread.c)
|
|
(void)vm;
|
|
}
|