mirror of
https://github.com/ruby/ruby.git
synced 2026-10-29 00:46:26 +08:00
RUBY_MN_THREADS=2: run the main thread as an M:N thread too
`RUBY_MN_THREADS=1` only affects threads created after it is read: the main thread keeps a native thread of its own and is woken through its condvar, so handing control to the main thread costs an order of magnitude more than handing it between any two other threads (6.4us against 0.56us for a Queue round trip here). With `RUBY_MN_THREADS=2` the running main thread is turned into an M:N thread in place (thread_sched_main_to_shared), rather than spawning a thread for the program body and leaving the main native thread idle: * the process stack becomes the main thread's coroutine context (coroutine_initialize_main), the way nt_start's own stack is a shared thread's context; * the main native thread joins the shared pool with a stack of its own for its scheduling loop (nt_loop_co), which starts on the first transfer into it -- the main thread's first park. From then on any shared native thread may resume the main thread, and this one serves any Ractor. nt_start's shared loop is split out as nt_shared_loop so that both entries share it. The main native thread never retires, its loop being on a stack only it could free: ractor_sched_deq takes a can_retire flag and native_thread_dedicated_dec always lets it rejoin. The wait in rb_ractor_terminate_all had to give its native thread back first, or exit hung with the Ractors it waits for having nothing to run on; that is the commit before this one. The VM barrier waits natively in the same way and has the same exposure, and is not touched here, being no more reachable under `=2` than before. Several places assumed the main thread owns the process's initial native thread: * native_thread_init_stack set the process stack's range for any thread starting on that native thread, over the pool stack an M:N thread already owns; the case is now keyed on th->sched.context. * thread_sched_switch passed to_dead for a thread whose status is THREAD_KILLED, which the main thread already is while rb_ractor_terminate_all parks it. A thread ends only through co_start's epilogue transfer. * thread_sched_atfork left the forker's nt->running_th and nt->retiring as the parent had them; retire eligibility is decided against the process's main native thread, refreshed there. * rb_thread_free_native_thread (RUBY_FREE_AT_EXIT) destroyed the native thread hosting the main thread even when shared, along with the altstack registered on the thread running it. RUBY_FREE_AT_EXIT now leaves more behind: the main native thread and its context and stack, since a thread is parked in its loop until the process ends, and the main thread's own coroutine context, since the free-at-exit path does not reach rb_threadptr_sched_free. Leak-checker baselines move by that much. The OS thread name is no longer set from the Ruby thread for M:N threads. One shared native thread runs many Ruby threads over its life, so the name described whichever one happened to start on it; under `=2` that renamed the process itself, the thread group leader's comm being what ps and pkill show. Thread#name= was already skipped for M:N threads for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d764e62a3
commit
ce6f200972
4 files changed
+329
-135
Notes:
git
2026-09-08 03:25:46 +00:00
No files matched your search
@@ -573,7 +573,12 @@ rb_thread_free_native_thread(void *th_ptr)
|
||||
{
|
||||
rb_thread_t *th = th_ptr;
|
||||
|
||||
native_thread_destroy_atfork(th->nt);
|
||||
// A thread with a coroutine context does not own its native thread: that
|
||||
// one is in the shared pool, listed there and with its altstack registered
|
||||
// on whichever pthread is running this. See rb_threadptr_sched_free().
|
||||
if (th->sched.context == NULL) {
|
||||
native_thread_destroy_atfork(th->nt);
|
||||
}
|
||||
th->nt = NULL;
|
||||
}
|
||||
|
||||
|
||||
+42
-9
@@ -637,6 +637,29 @@ static struct {
|
||||
VALUE *stack_start;
|
||||
} native_main_thread;
|
||||
|
||||
// Whether the calling native thread may leave the shared pool. The process's
|
||||
// main pthread cannot: under RUBY_MN_THREADS=2 its loop runs on a stack only
|
||||
// it could free (thread_sched_main_to_shared). By pthread_self(), not
|
||||
// nt->thread_id, which pthread_create may still be writing.
|
||||
static bool
|
||||
native_thread_self_can_retire_p(void)
|
||||
{
|
||||
return !pthread_equal(pthread_self(), native_main_thread.id);
|
||||
}
|
||||
|
||||
#if defined(HAVE_WORKING_FORK)
|
||||
// The forking thread's pthread is the child's only one, hence its main one
|
||||
static void
|
||||
native_main_thread_atfork(void)
|
||||
{
|
||||
native_main_thread.id = pthread_self();
|
||||
// The stack recorded here is the parent's initial one; this thread's is
|
||||
// another. Nothing reads it for a thread that is already running, and
|
||||
// saying "unknown" beats saying the wrong bounds.
|
||||
native_main_thread.stack_maxsize = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef STACK_END_ADDRESS
|
||||
extern void *STACK_END_ADDRESS;
|
||||
#endif
|
||||
@@ -739,21 +762,24 @@ native_thread_init_stack(rb_thread_t *th, void *local_in_parent_frame)
|
||||
native_thread_init_main_thread_stack(local_in_parent_frame);
|
||||
}
|
||||
|
||||
if (pthread_equal(curr, native_main_thread.id)) {
|
||||
if (th->sched.context != NULL) {
|
||||
// an M:N thread runs on the pool stack native_thread_create_shared
|
||||
// recorded, whichever native thread (the main one included) hosts it.
|
||||
// Not by nt->dedicated: a RESUMED hook may have pinned the nt already.
|
||||
}
|
||||
else if (pthread_equal(curr, native_main_thread.id)) {
|
||||
th->ec->machine.stack_start = native_main_thread.stack_start;
|
||||
th->ec->machine.stack_maxsize = native_main_thread.stack_maxsize;
|
||||
}
|
||||
else {
|
||||
#ifdef STACKADDR_AVAILABLE
|
||||
if (th_has_dedicated_nt(th)) {
|
||||
void *start;
|
||||
size_t size;
|
||||
void *start;
|
||||
size_t size;
|
||||
|
||||
if (get_stack(&start, &size) == 0) {
|
||||
uintptr_t diff = (uintptr_t)start - (uintptr_t)local_in_parent_frame;
|
||||
th->ec->machine.stack_start = local_in_parent_frame;
|
||||
th->ec->machine.stack_maxsize = size - diff;
|
||||
}
|
||||
if (get_stack(&start, &size) == 0) {
|
||||
uintptr_t diff = (uintptr_t)start - (uintptr_t)local_in_parent_frame;
|
||||
th->ec->machine.stack_start = local_in_parent_frame;
|
||||
th->ec->machine.stack_maxsize = size - diff;
|
||||
}
|
||||
#else
|
||||
rb_raise(rb_eNotImpError, "ruby engine can initialize only in the main thread");
|
||||
@@ -1031,6 +1057,13 @@ native_set_thread_name(rb_thread_t *th)
|
||||
{
|
||||
#ifdef SET_CURRENT_THREAD_NAME
|
||||
VALUE loc;
|
||||
|
||||
// An M:N thread does not own the native thread it runs on: naming it here
|
||||
// would name whichever nt started it (under RUBY_MN_THREADS=2 that can be
|
||||
// the process's main one, whose name is the process's). Thread#name= is
|
||||
// skipped for the same reason (rb_thread_setname).
|
||||
if (!th->has_dedicated_nt) return;
|
||||
|
||||
if (!NIL_P(loc = th->name)) {
|
||||
SET_CURRENT_THREAD_NAME(RSTRING_PTR(loc));
|
||||
}
|
||||
|
||||
+275
-125
@@ -104,9 +104,17 @@ static bool timeslice_scan(rb_vm_t *vm, bool interrupt);
|
||||
static void timer_thread_wakeup(void);
|
||||
static void timer_thread_wakeup_locked(rb_vm_t *vm);
|
||||
static void timer_thread_wakeup_force(void);
|
||||
static void nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt);
|
||||
static void nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt);
|
||||
static bool nt_shared_loop(struct rb_native_thread *nt);
|
||||
static bool native_thread_self_can_retire_p(void);
|
||||
|
||||
#include THREAD_IMPL_SRC
|
||||
|
||||
#if USE_MN_THREADS
|
||||
static void thread_sched_main_to_shared(rb_thread_t *th);
|
||||
#endif
|
||||
|
||||
// Defaults for what the platform above did not opt out of.
|
||||
|
||||
#ifndef RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF
|
||||
@@ -1206,7 +1214,9 @@ thread_sched_switch(rb_thread_t *cth, rb_thread_t *next_th)
|
||||
struct rb_native_thread *nt = cth->nt;
|
||||
native_thread_assign(NULL, cth);
|
||||
RUBY_DEBUG_LOG("th:%u->%u on nt:%d", rb_th_serial(cth), rb_th_serial(next_th), nt->serial);
|
||||
thread_sched_switch0(cth->sched.context, next_th, nt, cth->status == THREAD_KILLED);
|
||||
// never final: a thread ends only through co_start's epilogue transfer. The
|
||||
// main thread is THREAD_KILLED (rb_ec_cleanup) while it still parks here.
|
||||
thread_sched_switch0(cth->sched.context, next_th, nt, false);
|
||||
}
|
||||
|
||||
#if VM_CHECK_MODE > 0
|
||||
@@ -1344,7 +1354,7 @@ ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r)
|
||||
#define SNT_KEEP_MINIMUM (MINIMUM_SNT > 1 ? MINIMUM_SNT : 1)
|
||||
|
||||
static rb_ractor_t *
|
||||
ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr)
|
||||
ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr, bool can_retire)
|
||||
{
|
||||
rb_ractor_t *r;
|
||||
int idle_streak = 0; // consecutive pops that found the queue empty
|
||||
@@ -1360,7 +1370,7 @@ ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr)
|
||||
while ((r = ccan_list_pop(&vm->ractor.sched.grq, rb_ractor_t, threads.sched.grq_node)) == NULL) {
|
||||
RUBY_DEBUG_LOG("wait grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
|
||||
|
||||
if (SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
|
||||
if (can_retire && SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
|
||||
(int)RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) > SNT_KEEP_MINIMUM) {
|
||||
RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
|
||||
RUBY_DEBUG_LOG("retire, snt_cnt:%d", (int)vm->ractor.sched.snt_cnt);
|
||||
@@ -1753,6 +1763,9 @@ thread_sched_atfork(struct rb_thread_sched *sched)
|
||||
rb_native_mutex_initialize(&vm->ractor.sched.timeslice.lock);
|
||||
ccan_list_head_init(&vm->ractor.sched.timeslice.scheds);
|
||||
rb_native_mutex_initialize(&th->nt->running_th_lock); // a scan could hold it at fork
|
||||
th->nt->running_th = NULL; // th re-records itself below
|
||||
th->nt->retiring = false; // the pool it had no room in is gone
|
||||
native_main_thread_atfork(); // this pthread is the process's main one now
|
||||
// Fork can copy nodes linked (or torn mid-link); re-init every sched's
|
||||
// node so rb_thread_sched_destroy's del_init stays a no-op for them.
|
||||
rb_ractor_t *r;
|
||||
@@ -1800,14 +1813,17 @@ ruby_mn_threads_params(void)
|
||||
rb_vm_t *vm = GET_VM();
|
||||
rb_ractor_t *main_ractor = GET_RACTOR();
|
||||
|
||||
// RUBY_MN_THREADS: 1 = threads created in the main Ractor are M:N,
|
||||
// 2 = the main thread too (see thread_sched_main_to_shared)
|
||||
const char *mn_threads_cstr = getenv("RUBY_MN_THREADS");
|
||||
bool enable_mn_threads = false;
|
||||
int mn_threads = (USE_MN_THREADS && mn_threads_cstr) ? atoi(mn_threads_cstr) : 0;
|
||||
|
||||
if (USE_MN_THREADS && mn_threads_cstr && (enable_mn_threads = atoi(mn_threads_cstr) > 0)) {
|
||||
// enabled
|
||||
ruby_mn_threads_enabled = 1;
|
||||
if (mn_threads > 0) {
|
||||
ruby_mn_threads_enabled = mn_threads;
|
||||
}
|
||||
main_ractor->threads.sched.enable_mn_threads = enable_mn_threads;
|
||||
// =2 publishes it from thread_sched_main_to_shared, with the main nt's
|
||||
// pool entry, so that the timer thread cannot mint an snt in between
|
||||
main_ractor->threads.sched.enable_mn_threads = mn_threads == 1;
|
||||
|
||||
const char *max_cpu_cstr = getenv("RUBY_MAX_CPU");
|
||||
int max_cpu = native_thread_default_max_cpu();
|
||||
@@ -1820,6 +1836,12 @@ ruby_mn_threads_params(void)
|
||||
}
|
||||
|
||||
vm->ractor.sched.max_cpu = max_cpu;
|
||||
|
||||
#if USE_MN_THREADS
|
||||
if (mn_threads >= 2) {
|
||||
thread_sched_main_to_shared(GET_THREAD());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1857,10 +1879,13 @@ native_thread_dedicated_dec(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_threa
|
||||
|
||||
if (nt->dedicated == 0) {
|
||||
// Rejoin under the max_cpu cap; with no room this nt retires and
|
||||
// belongs to neither count until it ends.
|
||||
// belongs to neither count until it ends. The process's main nt
|
||||
// cannot end (its loop runs on a stack it would have to free), so
|
||||
// it always rejoins.
|
||||
const bool can_retire = native_thread_self_can_retire_p();
|
||||
while (1) {
|
||||
rb_atomic_t snt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
|
||||
if (snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
|
||||
if (!can_retire || snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
|
||||
if (RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt, snt + 1) == snt) break;
|
||||
}
|
||||
else {
|
||||
@@ -1958,123 +1983,240 @@ nt_start(void *ptr)
|
||||
|
||||
RUBY_DEBUG_LOG("nt:%u", nt->serial);
|
||||
|
||||
bool in_snts = false;
|
||||
if (nt->dedicated) {
|
||||
// wait running turn
|
||||
rb_thread_t *th = nt->running_thread;
|
||||
struct rb_thread_sched *sched = TH_SCHED(th);
|
||||
|
||||
if (!nt->dedicated) {
|
||||
RUBY_DEBUG_LOG("on dedicated th:%u", rb_th_serial(th));
|
||||
ruby_thread_set_native(th);
|
||||
|
||||
thread_sched_lock(sched, th);
|
||||
{
|
||||
if (sched->running == th) {
|
||||
thread_sched_add_running_thread(sched, th);
|
||||
}
|
||||
thread_sched_wait_running_turn(sched, th, false, NULL);
|
||||
}
|
||||
thread_sched_unlock(sched, th);
|
||||
|
||||
// start threads
|
||||
call_thread_start_func_2(th);
|
||||
// TODO: allow to change to the SNT
|
||||
}
|
||||
else {
|
||||
coroutine_initialize_main(nt->nt_context);
|
||||
nt_snts_join(vm, nt);
|
||||
|
||||
// join the snt list that the barrier/timeslice scans walk
|
||||
rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
|
||||
{
|
||||
ccan_list_add(&vm->ractor.sched.ntlist.snts, &nt->snts_node);
|
||||
bool retired = nt_shared_loop(nt);
|
||||
nt_snts_leave(vm, nt);
|
||||
|
||||
if (retired) {
|
||||
// The counts dropped this nt already; nothing can reference it now.
|
||||
RUBY_DEBUG_LOG("retired nt:%u", nt->serial);
|
||||
native_thread_destroy_self(nt);
|
||||
}
|
||||
rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
|
||||
in_snts = true;
|
||||
}
|
||||
|
||||
bool retired = false;
|
||||
|
||||
while (1) {
|
||||
if (nt->dedicated) {
|
||||
// wait running turn
|
||||
rb_thread_t *th = nt->running_thread;
|
||||
struct rb_thread_sched *sched = TH_SCHED(th);
|
||||
|
||||
RUBY_DEBUG_LOG("on dedicated th:%u", rb_th_serial(th));
|
||||
ruby_thread_set_native(th);
|
||||
|
||||
thread_sched_lock(sched, th);
|
||||
{
|
||||
if (sched->running == th) {
|
||||
thread_sched_add_running_thread(sched, th);
|
||||
}
|
||||
thread_sched_wait_running_turn(sched, th, false, NULL);
|
||||
}
|
||||
thread_sched_unlock(sched, th);
|
||||
|
||||
// start threads
|
||||
call_thread_start_func_2(th);
|
||||
break; // TODO: allow to change to the SNT
|
||||
}
|
||||
else {
|
||||
RUBY_DEBUG_LOG("check next");
|
||||
if (nt->retiring) { // came back with no room in the shared pool
|
||||
retired = true;
|
||||
break;
|
||||
}
|
||||
|
||||
rb_ractor_t *r = ractor_sched_deq(vm, NULL);
|
||||
|
||||
if (r) {
|
||||
struct rb_thread_sched *sched = &r->threads.sched;
|
||||
|
||||
bool locked = true;
|
||||
|
||||
thread_sched_lock(sched, NULL);
|
||||
{
|
||||
rb_thread_t *next_th = sched->running;
|
||||
|
||||
if (next_th && next_th->nt == NULL) {
|
||||
RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
|
||||
#if USE_MN_THREADS
|
||||
thread_sched_switch0(nt->nt_context, next_th, nt, false);
|
||||
|
||||
// If a coroutine terminated during the transfer, co_start
|
||||
// recorded it in nt->dead_co (switch0's return value is
|
||||
// backend-dependent, unusable; see thread_pthread.h).
|
||||
struct coroutine_context *dead_co = nt->dead_co;
|
||||
nt->dead_co = NULL;
|
||||
if (thread_sched_reclaim(dead_co)) {
|
||||
// it already released the sched lock before its
|
||||
// transfer (its Ractor may be gone): leave sched be.
|
||||
locked = false;
|
||||
}
|
||||
#else
|
||||
thread_sched_switch0(nt->nt_context, next_th, nt, false);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
RUBY_DEBUG_LOG("no schedulable threads -- next_th:%p", next_th);
|
||||
}
|
||||
}
|
||||
if (locked) {
|
||||
thread_sched_unlock(sched, NULL);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ractor_sched_deq retired this nt.
|
||||
retired = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (nt->dedicated) {
|
||||
// SNT becomes DNT while running
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in_snts) {
|
||||
// Leaving the shared loop: every path back here deregistered first
|
||||
// (park and death both precede the transfer), so only the snts entry
|
||||
// is left to remove.
|
||||
VM_ASSERT(nt->running_th == NULL);
|
||||
rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
|
||||
{
|
||||
ccan_list_del_init(&nt->snts_node);
|
||||
}
|
||||
rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
|
||||
}
|
||||
|
||||
if (retired) {
|
||||
// The counts dropped this nt already; nothing can reference it now.
|
||||
RUBY_DEBUG_LOG("retired nt:%u", nt->serial);
|
||||
native_thread_destroy_self(nt);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// join the snt list that the barrier/timeslice scans walk
|
||||
static void
|
||||
nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt)
|
||||
{
|
||||
rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
|
||||
{
|
||||
ccan_list_add(&vm->ractor.sched.ntlist.snts, &nt->snts_node);
|
||||
}
|
||||
rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
|
||||
}
|
||||
|
||||
// Leaving the shared loop: every path back here deregistered first
|
||||
// (park and death both precede the transfer), so only the snts entry
|
||||
// is left to remove.
|
||||
static void
|
||||
nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt)
|
||||
{
|
||||
VM_ASSERT(nt->running_th == NULL);
|
||||
rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
|
||||
{
|
||||
ccan_list_del_init(&nt->snts_node);
|
||||
}
|
||||
rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
|
||||
}
|
||||
|
||||
// The shared nt's scheduling loop: serve Ractors from the grq until this nt
|
||||
// retires (returns true) or goes dedicated while running (returns false).
|
||||
static bool
|
||||
nt_shared_loop(struct rb_native_thread *nt)
|
||||
{
|
||||
rb_vm_t *vm = nt->vm;
|
||||
|
||||
while (1) {
|
||||
RUBY_DEBUG_LOG("check next");
|
||||
if (nt->retiring) { // came back with no room in the shared pool
|
||||
return true;
|
||||
}
|
||||
|
||||
// asked every time: a fork leaves this loop's native thread as the
|
||||
// child's main one, which may not retire
|
||||
rb_ractor_t *r = ractor_sched_deq(vm, NULL, native_thread_self_can_retire_p());
|
||||
|
||||
if (r) {
|
||||
struct rb_thread_sched *sched = &r->threads.sched;
|
||||
|
||||
bool locked = true;
|
||||
|
||||
thread_sched_lock(sched, NULL);
|
||||
{
|
||||
rb_thread_t *next_th = sched->running;
|
||||
|
||||
if (next_th && next_th->nt == NULL) {
|
||||
RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
|
||||
#if USE_MN_THREADS
|
||||
thread_sched_switch0(nt->nt_context, next_th, nt, false);
|
||||
|
||||
// If a coroutine terminated during the transfer, co_start
|
||||
// recorded it in nt->dead_co (switch0's return value is
|
||||
// backend-dependent, unusable; see thread_pthread.h).
|
||||
struct coroutine_context *dead_co = nt->dead_co;
|
||||
nt->dead_co = NULL;
|
||||
if (thread_sched_reclaim(dead_co)) {
|
||||
// it already released the sched lock before its
|
||||
// transfer (its Ractor may be gone): leave sched be.
|
||||
locked = false;
|
||||
}
|
||||
#else
|
||||
thread_sched_switch0(nt->nt_context, next_th, nt, false);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
RUBY_DEBUG_LOG("no schedulable threads -- next_th:%p", next_th);
|
||||
}
|
||||
}
|
||||
if (locked) {
|
||||
thread_sched_unlock(sched, NULL);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ractor_sched_deq retired this nt.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nt->dedicated) {
|
||||
// SNT becomes DNT while running
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_MN_THREADS
|
||||
// The scheduling loop of the process's main nt, entered when RUBY_MN_THREADS=2
|
||||
// turned it shared: the process stack belongs to the main thread's context,
|
||||
// so this loop runs on a coroutine of its own (thread_sched_main_to_shared).
|
||||
static COROUTINE
|
||||
nt_loop_co(struct coroutine_context *from, struct coroutine_context *self)
|
||||
{
|
||||
#ifdef RUBY_ASAN_ENABLED
|
||||
__sanitizer_finish_switch_fiber(self->fake_stack,
|
||||
(const void**)&from->stack_base, &from->stack_size);
|
||||
#endif
|
||||
struct rb_native_thread *nt = (struct rb_native_thread *)self->argument;
|
||||
|
||||
// The first entry is a transfer that in nt_shared_loop would return from
|
||||
// thread_sched_switch0: a parked thread left its sched lock held for the
|
||||
// loop to release, or a terminated one (dead_co) released it itself.
|
||||
struct coroutine_context *dead_co = nt->dead_co;
|
||||
nt->dead_co = NULL;
|
||||
if (!thread_sched_reclaim(dead_co)) {
|
||||
rb_thread_t *parked_th = (rb_thread_t *)from->argument;
|
||||
thread_sched_unlock(TH_SCHED(parked_th), NULL);
|
||||
}
|
||||
|
||||
// as after a switch0 return: the thread may have pinned this nt
|
||||
// (rb_thread_lock_native_thread) before it ended
|
||||
if (!nt->dedicated) {
|
||||
if (nt_shared_loop(nt)) rb_bug("main nt retired"); // native_thread_self_can_retire_p() is false here
|
||||
}
|
||||
nt_snts_leave(nt->vm, nt);
|
||||
|
||||
// Went dedicated while running (rb_thread_lock_native_thread) and that
|
||||
// thread ended. Nothing can resume this context; sleep out the process.
|
||||
while (1) {
|
||||
pause();
|
||||
}
|
||||
}
|
||||
|
||||
// RUBY_MN_THREADS=2: make the running main thread an M:N thread in place.
|
||||
// Its context becomes the process stack (as nt_start's own stack is an snt's
|
||||
// context) and its nt joins the shared pool with a fresh stack for its loop.
|
||||
// Nothing changes for the thread until it first blocks: that transfer
|
||||
// starts nt_loop_co on this nt, and any snt may resume the thread later.
|
||||
static void
|
||||
thread_sched_main_to_shared(rb_thread_t *th)
|
||||
{
|
||||
rb_vm_t *vm = th->vm;
|
||||
struct rb_native_thread *nt = th->nt;
|
||||
struct rb_thread_sched *sched = TH_SCHED(th);
|
||||
|
||||
VM_ASSERT(th == vm->ractor.main_thread);
|
||||
VM_ASSERT(nt->dedicated == 1 && th->has_dedicated_nt);
|
||||
VM_ASSERT(sched->running == th);
|
||||
|
||||
// the loop's stack (the vm stack half of the pool slot goes unused)
|
||||
void *vm_stack, *machine_stack;
|
||||
int err = nt_alloc_stack(vm, &vm_stack, &machine_stack);
|
||||
if (err) {
|
||||
rb_warn("RUBY_MN_THREADS=2: cannot allocate the main nt's stack (%s); the main thread stays dedicated", strerror(err));
|
||||
th->ractor->threads.sched.enable_mn_threads = true; // as =1
|
||||
return;
|
||||
}
|
||||
size_t machine_stack_size = vm->default_params.thread_machine_stack_size - sizeof(struct nt_machine_stack_footer);
|
||||
// the main nt is ZALLOC'd by Init_BareVM, not native_thread_alloc: no context yet
|
||||
nt->nt_context = ruby_xmalloc(sizeof(struct coroutine_context));
|
||||
coroutine_initialize(nt->nt_context, nt_loop_co, machine_stack, machine_stack_size);
|
||||
nt->nt_context->argument = nt;
|
||||
|
||||
// the thread's context is the process stack it already runs on
|
||||
struct rb_thread_context *tctx = ruby_xmalloc(sizeof(struct rb_thread_context));
|
||||
tctx->stack = NULL; // not a pool stack: never freed
|
||||
tctx->dead = false;
|
||||
tctx->nt = NULL;
|
||||
coroutine_initialize_main(&tctx->co);
|
||||
tctx->co.argument = th;
|
||||
th->sched.context = &tctx->co;
|
||||
|
||||
thread_sched_lock(sched, th);
|
||||
{
|
||||
// re-record the running thread as an snt's (running_dnts -> nt->running_th)
|
||||
thread_sched_del_running_thread(sched, th);
|
||||
nt->dedicated = 0;
|
||||
th->has_dedicated_nt = 0;
|
||||
nt_snts_join(vm, nt);
|
||||
// the pool's first snt; under the lock native_thread_check_and_create_shared
|
||||
// takes, so that it never mints one for the main Ractor alongside
|
||||
ractor_sched_lock(vm, th->ractor);
|
||||
{
|
||||
// The pool is still empty: the timer thread has been up since
|
||||
// Init_Thread and would mint one here, but its timeout branch is
|
||||
// the only path there and it sleeps untimed with nothing waiting.
|
||||
VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0);
|
||||
RUBY_ATOMIC_INC(vm->ractor.sched.snt_cnt);
|
||||
th->ractor->threads.sched.enable_mn_threads = true;
|
||||
}
|
||||
ractor_sched_unlock(vm, th->ractor);
|
||||
#if USE_RUBY_DEBUG_LOG
|
||||
vm->ractor.sched.dnt_cnt--;
|
||||
#endif
|
||||
thread_sched_add_running_thread(sched, th);
|
||||
}
|
||||
thread_sched_unlock(sched, th);
|
||||
|
||||
RUBY_DEBUG_LOG("main th:%u on nt:%d is now shared", rb_th_serial(th), nt->serial);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int native_thread_create_shared(rb_thread_t *th);
|
||||
|
||||
#if USE_MN_THREADS
|
||||
@@ -2115,13 +2257,15 @@ rb_threadptr_sched_free(rb_thread_t *th)
|
||||
{
|
||||
timer_thread_wake_fence(th);
|
||||
#if USE_MN_THREADS
|
||||
if (th->sched.malloc_stack) {
|
||||
// has dedicated
|
||||
SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
|
||||
native_thread_destroy(th->nt);
|
||||
}
|
||||
else if (th->sched.context != NULL) {
|
||||
// a coroutine thread that never reached its epilogue (never started);
|
||||
// A thread with a coroutine context runs on a native thread it shares and
|
||||
// does not own: one made for the shared pool, or the main thread made
|
||||
// shared. nt->dedicated does not say so, rb_thread_lock_native_thread()
|
||||
// raising it on a shared native thread that stays in the pool.
|
||||
const bool owns_nt = th->sched.context == NULL;
|
||||
|
||||
if (th->sched.context != NULL) {
|
||||
// a coroutine thread that never reached its epilogue (never started),
|
||||
// or the main thread made shared (its stack is the process stack);
|
||||
// a terminated one is reclaimed by whoever resumed from its final
|
||||
// transfer (thread_sched_reclaim), and cleared this pointer.
|
||||
struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context;
|
||||
@@ -2130,6 +2274,12 @@ rb_threadptr_sched_free(rb_thread_t *th)
|
||||
th->sched.context = NULL;
|
||||
// TODO: how to free nt and nt->altstack?
|
||||
}
|
||||
if (th->sched.malloc_stack) {
|
||||
SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
|
||||
if (th->nt && owns_nt) {
|
||||
native_thread_destroy(th->nt);
|
||||
}
|
||||
}
|
||||
#else
|
||||
SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
|
||||
native_thread_destroy(th->nt);
|
||||
|
||||
@@ -872,6 +872,12 @@ native_reset_timer_thread(void)
|
||||
* the ones that rb_bug().
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static bool
|
||||
native_thread_self_can_retire_p(void)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
native_thread_create_shared(rb_thread_t *th)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user