Share one thread/ractor scheduler between POSIX and Windows

Windows carried a second, much weaker scheduler: a single Win32 mutex as
the GVL, vm->clock for preemption, its own VM barrier over
vm->ractor.sync.barrier_*, its own Ractor wait/wakeup on a per-Ractor
condvar, no ready queue and no time slice. Keeping two made the rest of
the interpreter branch on which one was compiled in, and left Ractors on
Windows without real scheduling.

Move the scheduler out of thread_pthread.c into thread_sched.c and let
both platforms use it. thread.c now includes thread_sched.c, which
declares the scheduler <-> platform contract, includes THREAD_IMPL_SRC,
and then builds the scheduler on the primitives that file provides.
thread_pthread.c keeps only those primitives: synchronization, thread
local storage, native thread creation and stacks, the communication pipe,
the timer thread backend, the fork lock and the event hooks.

The data structures the scheduler works on move to a new thread_sched.h,
included from both platform headers, so struct rb_thread_sched,
struct rb_native_thread and struct rb_thread_sched_item are now shared.

thread_pthread_mn.c becomes thread_sched_mn.c: it is the M:N scheduler,
not a pthread detail. Most of it -- the coroutine threads, the native
thread stack pool, the timer wheel and the fd -> waiters map -- is
platform independent; only the readiness backend is not, and that section
is now marked as the seam where an epoll/kqueue split (or an IOCP backend,
which is what Windows would need for M:N) would go.

What Windows had to add is small: native_thread_interrupt(),
native_thread_default_max_cpu(), timer_thread_polling(),
timer_thread_wakeup_force(), RB_NATIVE_COND_MONOTONIC_P() and
RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF() -- the last because a
CRITICAL_SECTION is recursive, so trylock cannot back a "somebody holds
this" assertion. It sets USE_MN_THREADS to 0 and supplies the stubs
thread_sched_mn.c provides elsewhere, so every thread there is dedicated,
as it already was.

With one scheduler the platform branches go away: all 16 uses of
RUBY_THREAD_PTHREAD_H / RUBY_THREAD_WIN32_H are removed from vm_core.h,
ractor.c, ractor_core.h, ractor_sync.c, vm_sync.c, vm.c and thread.c.
ractor_check_blocking() went with them -- it only ever fed the win32
scheduler's vm->ractor.blocking_cnt -- and so did USE_VM_CLOCK, now that
the common time slice drives preemption on Windows too.

Verified on both platforms. Linux: 11 rounds of make test-all (36236
tests) and btest (2066 tests) with no failures, plus 474 runs of a
scheduler and Ractor stress soak. Windows: no test fails that does not
also fail on the same tree without this change, btest passes, and two
40-minute soaks of 438 and 460 runs are clean. Ractors now run in
parallel there -- 3.6x on four Ractors, where before they could not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Koichi SasadaandClaude Opus 5 committed 2026-08-29 04:04:48 +09:00
1 parent 46e53809f5
commit 90e729c9bc
16 files changed
+3351 -3343
Notes: git 2026-08-28 19:05:26 +00:00

No files matched your search

-56
View File
@@ -418,9 +418,7 @@ free_targeted_hooks(st_table *hooks_tbl)
st_foreach(hooks_tbl, free_targeted_hook_lists, 0);
}
#ifdef RUBY_THREAD_PTHREAD_H
void rb_thread_sched_destroy(struct rb_thread_sched *);
#endif
static void
ractor_free(void *ptr)
@@ -429,13 +427,8 @@ ractor_free(void *ptr)
RUBY_DEBUG_LOG("free r:%d", rb_ractor_id(r));
free_targeted_hooks(&r->pub.targeted_hooks);
#ifdef RUBY_THREAD_PTHREAD_H
rb_thread_sched_destroy(&r->threads.sched);
#endif
rb_native_mutex_destroy(&r->sync.lock);
#ifdef RUBY_THREAD_WIN32_H
rb_native_cond_destroy(&r->sync.wakeup_cond);
#endif
ractor_local_storage_free(r);
rb_hook_list_free(&r->pub.hooks);
rb_st_free_embedded_table(&r->pub.targeted_hooks);
@@ -1024,39 +1017,6 @@ rb_vm_ractor_blocking_cnt_dec(rb_vm_t *vm, rb_ractor_t *cr, const char *file, in
ractor_status_set(cr, ractor_running);
}
static void
ractor_check_blocking(rb_ractor_t *cr, unsigned int remained_thread_cnt, const char *file, int line)
{
VM_ASSERT(cr == GET_RACTOR());
#ifdef RUBY_THREAD_PTHREAD_H
// vm->ractor.blocking_cnt is only consumed by the win32 scheduler; the
// pthread one must not pay a VM lock per blocking region for it. The
// running<->blocking status flips stop with it (all callers), matching
// rb_ractor_blocking_threads_dec skipping the reverse transition.
return;
#endif
RUBY_DEBUG_LOG2(file, line,
"cr->threads.cnt:%u cr->threads.blocking_cnt:%u vm->ractor.cnt:%u vm->ractor.blocking_cnt:%u",
cr->threads.cnt, cr->threads.blocking_cnt,
GET_VM()->ractor.cnt, GET_VM()->ractor.blocking_cnt);
VM_ASSERT(cr->threads.cnt >= cr->threads.blocking_cnt + 1);
if (remained_thread_cnt > 0 &&
// will be block
cr->threads.cnt == cr->threads.blocking_cnt + 1) {
// change ractor status: running -> blocking
rb_vm_t *vm = GET_VM();
RB_VM_LOCKING() {
rb_vm_ractor_blocking_cnt_inc(vm, cr, file, line);
}
}
}
/* Remove a child that never started (send_parameters failed during creation). The
* creator calls this (rb_ractor_living_threads_remove assumes the current Ractor);
* leaving the set and disowning the objspace share one VM-lock section, no window. */
@@ -1100,8 +1060,6 @@ rb_ractor_living_threads_remove(rb_ractor_t *cr, rb_thread_t *th)
{
VM_ASSERT(cr == GET_RACTOR());
RUBY_DEBUG_LOG("r->threads.cnt:%d--", cr->threads.cnt);
ractor_check_blocking(cr, cr->threads.cnt - 1, __FILE__, __LINE__);
if (cr->threads.cnt == 1) {
vm_remove_ractor(th->vm, cr);
@@ -1124,7 +1082,6 @@ rb_ractor_blocking_threads_inc(rb_ractor_t *cr, const char *file, int line)
VM_ASSERT(cr->threads.cnt > 0);
VM_ASSERT(cr == GET_RACTOR());
ractor_check_blocking(cr, cr->threads.cnt, __FILE__, __LINE__);
cr->threads.blocking_cnt++;
}
@@ -1137,17 +1094,6 @@ rb_ractor_blocking_threads_dec(rb_ractor_t *cr, const char *file, int line)
VM_ASSERT(cr == GET_RACTOR());
#ifndef RUBY_THREAD_PTHREAD_H
// see rb_ractor_blocking_threads_inc
if (cr->threads.cnt == cr->threads.blocking_cnt) {
rb_vm_t *vm = GET_VM();
RB_VM_LOCKING() {
rb_vm_ractor_blocking_cnt_dec(vm, cr, __FILE__, __LINE__);
}
}
#endif
cr->threads.blocking_cnt--;
}
@@ -1236,7 +1182,6 @@ rb_ractor_terminate_all(void)
rb_vm_ractor_blocking_cnt_inc(vm, cr, __FILE__, __LINE__);
rb_del_running_thread(rb_ec_thread_ptr(cr->threads.running_ec));
rb_vm_cond_timedwait(vm, &vm->ractor.sync.terminate_cond, 1000 /* ms */);
#ifdef RUBY_THREAD_PTHREAD_H
while (vm->ractor.sched.barrier_is_waiting) {
// A barrier is waiting. Threads relinquish the VM lock before joining the barrier and
// since we just acquired the VM lock back, we're blocking other threads from joining it.
@@ -1246,7 +1191,6 @@ rb_ractor_terminate_all(void)
unsigned int lev;
RB_VM_LOCK_ENTER_LEV_NB(&lev);
}
#endif
rb_add_running_thread(rb_ec_thread_ptr(cr->threads.running_ec));
rb_vm_ractor_blocking_cnt_dec(vm, cr, __FILE__, __LINE__);
-4
View File
@@ -24,10 +24,6 @@ struct rb_ractor_sync {
VALUE locked_by;
#endif
#ifndef RUBY_THREAD_PTHREAD_H
rb_nativethread_cond_t wakeup_cond;
#endif
// incoming messages
struct ractor_queue *recv_queue;
-70
View File
@@ -945,9 +945,6 @@ ractor_sync_init(rb_ractor_t *r)
// no receive is rebuilding a payload yet
#ifndef RUBY_THREAD_PTHREAD_H
rb_native_cond_initialize(&r->sync.wakeup_cond);
#endif
}
/* Create the default port. Call only after the Ractor joined vm->ractor.set, so the
@@ -1304,73 +1301,6 @@ basket_type_name(enum ractor_basket_type type)
#endif // USE_RUBY_DEBUG_LOG
#ifdef RUBY_THREAD_PTHREAD_H
//
#else // win32
static void
ractor_cond_wait(rb_ractor_t *r, const rb_hrtime_t *end)
{
#if RACTOR_CHECK_MODE > 0
VALUE locked_by = r->sync.locked_by;
r->sync.locked_by = Qnil;
#endif
if (end) {
rb_hrtime_t now = rb_hrtime_now();
rb_hrtime_t rel = *end > now ? *end - now : 0;
// the condvar takes msec: never round a live timeout down to 0
unsigned long msec = (unsigned long)(rel / RB_HRTIME_PER_MSEC);
rb_native_cond_timedwait(&r->sync.wakeup_cond, &r->sync.lock, msec > 0 ? msec : 1);
}
else {
rb_native_cond_wait(&r->sync.wakeup_cond, &r->sync.lock);
}
#if RACTOR_CHECK_MODE > 0
r->sync.locked_by = locked_by;
#endif
}
static void *
ractor_wait_no_gvl(void *ptr)
{
struct ractor_waiter *waiter = (struct ractor_waiter *)ptr;
rb_ractor_t *cr = waiter->th->ractor;
RACTOR_LOCK_SELF(cr);
{
if (waiter->wakeup_status == wakeup_none) {
ractor_cond_wait(cr, waiter->end);
}
}
RACTOR_UNLOCK_SELF(cr);
return NULL;
}
static void
rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ptr)
{
struct ractor_waiter *waiter = (struct ractor_waiter *)ptr;
RACTOR_UNLOCK(cr);
{
rb_nogvl(ractor_wait_no_gvl, waiter,
ubf, waiter,
RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_INTR_FAIL);
}
RACTOR_LOCK(cr);
}
static void
rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *th)
{
// ractor lock is acquired
rb_native_cond_broadcast(&r->sync.wakeup_cond);
}
#endif
static bool
ractor_wakeup_all(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status)
{
+11 -6
View File
@@ -278,12 +278,19 @@ MAYBE_UNUSED(NOINLINE(static int thread_start_func_2(rb_thread_t *th, VALUE *sta
MAYBE_UNUSED(static bool th_has_dedicated_nt(const rb_thread_t *th));
MAYBE_UNUSED(static int waitfd_to_waiting_flag(int wfd_event));
#include THREAD_IMPL_SRC
#ifdef RB_THREAD_SCHED_NONE
// The no-thread model is not a set of primitives under the common scheduler:
// it replaces the scheduler with stubs, so it stands alone.
# include THREAD_IMPL_SRC
#else
// The scheduler pulls in the platform implementation (THREAD_IMPL_SRC) itself:
// the platform primitives come first, the scheduler is built on top of them.
# include "thread_sched.c"
#endif
/*
* TODO: somebody with win32 knowledge should be able to get rid of
* timer-thread by busy-waiting on signals. And it should be possible
* to make the GVL in thread_pthread.c be platform-independent.
* timer-thread by busy-waiting on signals.
*/
#ifndef BUSY_WAIT_SIGNALS
# define BUSY_WAIT_SIGNALS (0)
@@ -841,7 +848,7 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start)
// 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 in thread_sched_mn.c).
coroutine_thread_terminated(th);
rb_ractor_postmortem_free(&pf);
return 0;
@@ -6112,9 +6119,7 @@ rb_check_deadlock(rb_ractor_t *r)
{
if (GET_THREAD()->vm->thread_ignore_deadlock) return;
#ifdef RUBY_THREAD_PTHREAD_H
if (r->threads.sched.readyq_cnt > 0) return;
#endif
int sleeper_num = rb_ractor_sleeper_thread_num(r);
int ltnum = rb_ractor_living_thread_num(r);
+20 -3
View File
@@ -42,12 +42,10 @@ rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
{
}
#if 0
static void
void
rb_thread_sched_destroy(struct rb_thread_sched *sched)
{
}
#endif
// Do nothing for mutex guard
void
@@ -311,6 +309,25 @@ rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr)
// do nothing
}
void
rb_ractor_sched_barrier_end(rb_vm_t *vm, rb_ractor_t *cr)
{
// do nothing
}
void
rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ptr)
{
// nothing can wake this: with no threads there is nobody to send
rb_bug("unreachable");
}
void
rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *th)
{
// do nothing
}
bool
+9 -5
View File
@@ -8,12 +8,16 @@
// based implementation in vm.c
#define RB_THREAD_LOCAL_SPECIFIER
struct rb_native_thread {
void *thread_id; // NULL
};
// This model brings its own scheduler stubs (thread_none.c) instead of the
// common one; thread.c keys off this.
#define RB_THREAD_SCHED_NONE 1
struct rb_thread_sched_item {};
struct rb_thread_sched {};
// The scheduler's types are shared with the threaded platforms: the code that
// reads them (vm_sync.c, ractor.c, thread.c) is compiled for every thread
// model, so it needs the real fields even here, where nothing ever runs
// concurrently and they all stay zero. thread_none.c only ever passes the
// structs around, never looks inside them.
#include "thread_sched.h"
RUBY_EXTERN struct rb_execution_context_struct *ruby_current_ec;
NOINLINE(struct rb_execution_context_struct *rb_current_ec_noinline(void)); // for assertions
+19 -2372
View File
File diff suppressed because it is too large. Load diff
+4 -177
View File
@@ -8,6 +8,9 @@
Copyright (C) 2004-2007 Koichi Sasada
This platform runs the common scheduler; see thread_sched.h for its data
structures and thread_sched.c for the implementation.
**********************************************************************/
#ifdef HAVE_PTHREAD_NP_H
@@ -22,178 +25,7 @@
# define RB_THREAD_CURRENT_EC_NOINLINE
#endif
// How a thread_sched_wait_events() wait ended. "unavailable" (could not be
// registered) is not "the event fired": the caller must fall back, not proceed.
enum thread_sched_wait_result {
thread_sched_wait_event, // an event the caller asked for fired
thread_sched_wait_timeout, // the timeout expired before any event
thread_sched_wait_unavailable, // not registered; the caller must fall back
};
// this data should be protected by timer_th.waiting_lock
struct rb_thread_sched_waiting {
enum thread_sched_waiting_flag {
thread_sched_waiting_none = 0x00,
thread_sched_waiting_timeout = 0x01,
thread_sched_waiting_io_read = 0x02,
thread_sched_waiting_io_write = 0x08,
thread_sched_waiting_io_force = 0x40, // ignore readable
} flags;
struct {
// should be compat with hrtime.h
#ifdef MY_RUBY_BUILD_MAY_TIME_TRAVEL
int128_t timeout;
#else
uint64_t timeout;
#endif
uint32_t event_serial;
int fd; // -1 for timeout only
int result;
} data;
// connected to a timer_th wheel slot (timed) or timer_th.waiting_untimed
struct ccan_list_node node;
/* which wheel slot `node` is on; meaningful only while flags has
* thread_sched_waiting_timeout */
uint8_t wheel_lvl;
uint8_t wheel_slot;
// connected to rb_fd_waiters.waiters of data.fd
struct ccan_list_node fd_node;
};
// One entry per fd with waiters; fds stay dense, so a table indexed by fd fits.
// Entries live in fixed chunks: growing must not move a live list head.
struct rb_fd_waiters {
struct ccan_list_head waiters; // rb_thread_sched_waiting.fd_node
// The io flags currently armed in epoll/kqueue for this fd: the union of
// what its waiters asked for.
uint32_t armed_flags;
// Bumped on full disarm. Events carry the generation they were armed with,
// so one queued before the fd was disarmed (and reused) is recognised.
uint32_t generation;
};
// per-Thread scheduler helper data
struct rb_thread_sched_item {
struct {
struct ccan_list_node ubf;
// connected to ractor->threads.sched.reqdyq
// locked by ractor->threads.sched.lock
struct ccan_list_node readyq;
// Indicates whether thread is on the readyq.
// There is no clear relationship between this and th->status.
bool is_ready;
} node;
struct rb_thread_sched_waiting waiting_reason;
uint32_t event_serial;
// wakes pending on this thread (timer thread or an fd shard claim);
// under timer_th.wake_pending_lock
uint32_t wake_pending_cnt;
// parked on its own condvar with a deadline; under the sched lock (see
// ubf_waiting). Always false for an M:N thread: its deadline lives on the
// timer wheel, and its early wake comes from the timer thread instead.
bool waiting_timed;
bool malloc_stack;
void *context_stack;
size_t context_stack_size;
struct coroutine_context *context;
};
struct rb_native_thread {
rb_atomic_t serial;
struct rb_vm_struct *vm;
rb_nativethread_id_t thread_id;
#ifdef RB_THREAD_T_HAS_NATIVE_ID
int tid;
#endif
struct rb_thread_struct *running_thread;
// The running thread on this shared nt, for the barrier/timeslice scans.
// While a scan holds running_th_lock the thread cannot finish parking.
rb_nativethread_lock_t running_th_lock;
struct rb_thread_struct *running_th;
struct ccan_list_node snts_node; // in vm->ractor.sched.ntlist.snts
// in vm->ractor.sched.ntlist.running_dnts while running_thread runs
struct ccan_list_node running_dnts_node;
// barrier_serial stamped by the barrier's counting walk; this nt's
// deregistration during that barrier decrements the snapshot count
uint32_t barrier_counted_serial;
// to control native thread; use sched->lock
rb_nativethread_cond_t readyq;
#ifdef USE_SIGALTSTACK
void *altstack;
#endif
struct coroutine_context *nt_context;
int dedicated;
// set when this thread came back from a blocking region with no room left
// in the shared pool; it ends when it next asks for work
bool retiring;
// A terminating coroutine records its context here before its final
// transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s
// return value: its meaning differs between the amd64 asm and ucontext.)
struct coroutine_context *dead_co;
};
#undef except
#undef try
#undef leave
#undef finally
// per-Ractor
struct rb_thread_sched {
rb_nativethread_lock_t lock_;
#if VM_CHECK_MODE
struct rb_thread_struct *lock_owner;
#endif
struct rb_thread_struct *running; // running thread or NULL
// Most recently running thread or NULL. If this thread wakes up before the newly running
// thread completes the transfer of control, it can interrupt and resume running.
// The new thread clears this field when it takes control.
struct rb_thread_struct *runnable_hot_th;
int runnable_hot_th_waiting;
bool is_running;
bool enable_mn_threads;
struct ccan_list_head readyq;
int readyq_cnt;
// ractor scheduling
// When not linked in vm->ractor.sched.grq, this node is kept
// self-linked (ccan_list_node_init), so "linked?" can be read off the
// node itself: enqueuers assert it, and direct transfers cancel an
// outstanding entry (see ractor_sched_cancel_enq).
struct ccan_list_node grq_node;
struct ccan_list_node timeslice_node; // self-linked = not on timeslice.scheds
};
struct rb_thread_context;
// A coroutine (M:N) thread's teardown runs coroutine_thread_terminated
// instead of the dedicated-thread path in thread_start_func_2; see the
// comments there and in thread_pthread_mn.c. th->sched.context is cleared in
// that epilogue, so this also reads as "did not tear down yet".
// (Only meaningful when USE_MN_THREADS -- gate uses accordingly; the macro
// itself is a plain pointer test and always compiles.)
#define th_has_coroutine(th) ((th)->sched.context != NULL)
#include "thread_sched.h"
#ifdef RB_THREAD_LOCAL_SPECIFIER
NOINLINE(void rb_current_ec_set(struct rb_execution_context_struct *));
@@ -228,9 +60,4 @@ native_tls_set(native_tls_key_t key, void *ptr)
RUBY_EXTERN native_tls_key_t ruby_current_ec_key;
#endif
struct rb_ractor_struct;
void rb_ractor_sched_wait(struct rb_execution_context_struct *ec, struct rb_ractor_struct *cr, rb_unblock_function_t *ubf, void *ptr);
void rb_ractor_sched_wakeup(struct rb_ractor_struct *r, struct rb_thread_struct *th);
void rb_thread_wake_fence(struct rb_thread_struct *th);
#endif /* RUBY_THREAD_PTHREAD_H */
+2501
View File
File diff suppressed because it is too large. Load diff
+264
View File
@@ -0,0 +1,264 @@
#ifndef RUBY_THREAD_SCHED_H
#define RUBY_THREAD_SCHED_H
/**********************************************************************
thread_sched.h - data structures of the thread/ractor scheduler
The scheduler itself lives in thread_sched.c and is shared by every
platform. This header holds the types it works on; it is included from
the platform header (thread_pthread.h / thread_win32.h), which adds the
platform specific members and the thread local storage plumbing.
== platform primitive layer ==
thread_sched.c is built on top of the following, which the platform
implementation (THREAD_IMPL_SRC) has to provide before including it:
* rb_native_mutex_*() / rb_native_cond_*() (ruby/thread_native.h)
* native_thread_create0() / native_thread_destroy() and friends
* native_thread_interrupt() -- poke a thread out of a blocking call
* timer_thread_polling() -- the timer thread's blocking wait
* timer_thread_wakeup_force() -- wake that wait up
* TIMER_THREAD_CREATED_P()
* USE_MN_THREADS -- 1 enables the M:N scheduler
(when 0 the platform supplies the stubs listed at the bottom of
thread_sched_mn.c)
**********************************************************************/
// How a thread_sched_wait_events() wait ended. "unavailable" (could not be
// registered) is not "the event fired": the caller must fall back, not proceed.
enum thread_sched_wait_result {
thread_sched_wait_event, // an event the caller asked for fired
thread_sched_wait_timeout, // the timeout expired before any event
thread_sched_wait_unavailable, // not registered; the caller must fall back
};
// this data should be protected by timer_th.waiting_lock
struct rb_thread_sched_waiting {
enum thread_sched_waiting_flag {
thread_sched_waiting_none = 0x00,
thread_sched_waiting_timeout = 0x01,
thread_sched_waiting_io_read = 0x02,
thread_sched_waiting_io_write = 0x08,
thread_sched_waiting_io_force = 0x40, // ignore readable
} flags;
struct {
// should be compat with hrtime.h
#ifdef MY_RUBY_BUILD_MAY_TIME_TRAVEL
int128_t timeout;
#else
uint64_t timeout;
#endif
uint32_t event_serial;
int fd; // -1 for timeout only
int result;
} data;
// connected to a timer_th wheel slot (timed) or timer_th.waiting_untimed
struct ccan_list_node node;
/* which wheel slot `node` is on; meaningful only while flags has
* thread_sched_waiting_timeout */
uint8_t wheel_lvl;
uint8_t wheel_slot;
// connected to rb_fd_waiters.waiters of data.fd
struct ccan_list_node fd_node;
};
// One entry per fd with waiters; fds stay dense, so a table indexed by fd fits.
// Entries live in fixed chunks: growing must not move a live list head.
struct rb_fd_waiters {
struct ccan_list_head waiters; // rb_thread_sched_waiting.fd_node
// The io flags currently armed in epoll/kqueue for this fd: the union of
// what its waiters asked for.
uint32_t armed_flags;
// Bumped on full disarm. Events carry the generation they were armed with,
// so one queued before the fd was disarmed (and reused) is recognised.
uint32_t generation;
};
// per-Thread scheduler helper data
struct rb_thread_sched_item {
struct {
struct ccan_list_node ubf;
// connected to ractor->threads.sched.reqdyq
// locked by ractor->threads.sched.lock
struct ccan_list_node readyq;
// Indicates whether thread is on the readyq.
// There is no clear relationship between this and th->status.
bool is_ready;
} node;
struct rb_thread_sched_waiting waiting_reason;
uint32_t event_serial;
// wakes pending on this thread (timer thread or an fd shard claim);
// under timer_th.wake_pending_lock
uint32_t wake_pending_cnt;
// parked on its own condvar with a deadline; under the sched lock (see
// ubf_waiting). Always false for an M:N thread: its deadline lives on the
// timer wheel, and its early wake comes from the timer thread instead.
bool waiting_timed;
bool malloc_stack;
void *context_stack;
size_t context_stack_size;
struct coroutine_context *context;
};
struct rb_native_thread {
rb_atomic_t serial;
struct rb_vm_struct *vm;
rb_nativethread_id_t thread_id;
#ifdef RB_THREAD_T_HAS_NATIVE_ID
int tid;
#endif
#if defined(_WIN32)
// signalled by native_thread_interrupt() to break this thread out of a
// blocking w32_wait_events()
HANDLE interrupt_event;
#endif
struct rb_thread_struct *running_thread;
// The running thread on this shared nt, for the barrier/timeslice scans.
// While a scan holds running_th_lock the thread cannot finish parking.
rb_nativethread_lock_t running_th_lock;
struct rb_thread_struct *running_th;
struct ccan_list_node snts_node; // in vm->ractor.sched.ntlist.snts
// in vm->ractor.sched.ntlist.running_dnts while running_thread runs
struct ccan_list_node running_dnts_node;
// barrier_serial stamped by the barrier's counting walk; this nt's
// deregistration during that barrier decrements the snapshot count
uint32_t barrier_counted_serial;
// to control native thread; use sched->lock
rb_nativethread_cond_t readyq;
#ifdef USE_SIGALTSTACK
void *altstack;
#endif
struct coroutine_context *nt_context;
int dedicated;
// set when this thread came back from a blocking region with no room left
// in the shared pool; it ends when it next asks for work
bool retiring;
// A terminating coroutine records its context here before its final
// transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s
// return value: its meaning differs between the amd64 asm and ucontext.)
struct coroutine_context *dead_co;
};
// <windows.h> defines these as macros, and the field names below (and in the
// rest of the interpreter) would be rewritten by them.
#undef except
#undef try
#undef leave
#undef finally
// per-Ractor
struct rb_thread_sched {
rb_nativethread_lock_t lock_;
#if VM_CHECK_MODE
struct rb_thread_struct *lock_owner;
#endif
struct rb_thread_struct *running; // running thread or NULL
// Most recently running thread or NULL. If this thread wakes up before the newly running
// thread completes the transfer of control, it can interrupt and resume running.
// The new thread clears this field when it takes control.
struct rb_thread_struct *runnable_hot_th;
int runnable_hot_th_waiting;
bool is_running;
bool enable_mn_threads;
struct ccan_list_head readyq;
int readyq_cnt;
// ractor scheduling
// When not linked in vm->ractor.sched.grq, this node is kept
// self-linked (ccan_list_node_init), so "linked?" can be read off the
// node itself: enqueuers assert it, and direct transfers cancel an
// outstanding entry (see ractor_sched_cancel_enq).
struct ccan_list_node grq_node;
struct ccan_list_node timeslice_node; // self-linked = not on timeslice.scheds
};
struct rb_thread_context;
// A coroutine (M:N) thread's teardown runs coroutine_thread_terminated
// instead of the dedicated-thread path in thread_start_func_2; see the
// comments there and in thread_sched_mn.c. th->sched.context is cleared in
// that epilogue, so this also reads as "did not tear down yet".
// (Only meaningful when USE_MN_THREADS -- gate uses accordingly; the macro
// itself is a plain pointer test and always compiles.)
#define th_has_coroutine(th) ((th)->sched.context != NULL)
struct rb_ractor_struct;
// VM wide: what schedules Ractors onto native threads. One per VM, in
// rb_vm_struct.ractor.sched.
struct rb_ractor_sched {
rb_nativethread_lock_t lock;
struct rb_ractor_struct *lock_owner;
bool locked;
rb_nativethread_cond_t cond; // GRQ
rb_atomic_t snt_cnt; // count of shared NTs; lock-free (see native_thread_dedicated_inc)
unsigned int dnt_cnt; // count of dedicated NTs; logging only (USE_RUBY_DEBUG_LOG), not atomic
unsigned int max_cpu;
struct ccan_list_head grq; // // Global Ready Queue
rb_atomic_t winding_cnt; // native threads between a coroutine epilogue and its reclaim; ruby_vm_destruct waits for 0
unsigned int grq_cnt;
// What the barrier walk visits: threads running on dedicated
// nts, and the shared nts (whose running_th fields hold the rest).
struct {
rb_nativethread_lock_t lock;
struct ccan_list_head running_dnts;
struct ccan_list_head snts;
} ntlist;
// scheds whose readyq holds waiters: the timer ticks their
// running thread (timeslice_scan) and prunes drained entries.
struct {
rb_nativethread_lock_t lock;
struct ccan_list_head scheds;
} timeslice;
// true if timeslice timer is not enable
bool timeslice_wait_inf;
// barrier
rb_nativethread_cond_t barrier_complete_cond;
rb_nativethread_cond_t barrier_release_cond;
// bool; nonzero while a stop-the-world section is active. Set
// before the barrier walks the running records; a record moved
// after the walk sees it (thread_sched_setup_running_threads).
rb_atomic_t barrier_is_waiting;
unsigned int barrier_joined_cnt; // threads joined so far; under sched.lock
unsigned int barrier_running_cnt; // runners counted by the barrier's walk; under sched.lock
unsigned int barrier_serial;
struct rb_ractor_struct *barrier_ractor;
unsigned int barrier_lock_rec;
};
void rb_ractor_sched_wait(struct rb_execution_context_struct *ec, struct rb_ractor_struct *cr, rb_unblock_function_t *ptr, void *arg);
void rb_ractor_sched_wakeup(struct rb_ractor_struct *r, struct rb_thread_struct *th);
void rb_thread_wake_fence(struct rb_thread_struct *th);
#endif /* RUBY_THREAD_SCHED_H */
+27 -1
View File
@@ -1,4 +1,22 @@
// included by "thread_pthread.c"
/* -*-c-*- */
/**********************************************************************
thread_sched_mn.c - the M:N scheduler
Included by the platform implementation (currently only thread_pthread.c)
when USE_MN_THREADS is 1. A platform that cannot run coroutine threads
defines it to 0 and supplies the stubs at the bottom of this file itself
(see thread_win32.c).
Most of what is here is platform independent: the coroutine threads
themselves, the native thread stack pool, the timer wheel, and the
fd -> waiters map. The part that is not is the readiness backend --
arming an fd and waiting for events -- which is epoll on Linux and kqueue
elsewhere. Those pieces are marked "backend" below; they are the natural
seam for a thread_sched_epoll.c / thread_sched_kqueue.c split, and for an
IOCP backend that would let Windows run M:N threads too.
**********************************************************************/
#if USE_MN_THREADS
@@ -1358,6 +1376,14 @@ verify_waiting_list(void)
#endif
}
/* ------------------------------------------------------------------------
* backend: the readiness notification mechanism (epoll / kqueue).
*
* Everything below that names epoll or kqueue is this backend; the rest of
* the M:N scheduler only asks it to arm an fd (fd_waiters_arm) and to wait
* for what fired (event_wait / timer_thread_polling).
* ------------------------------------------------------------------------ */
#if HAVE_SYS_EVENT_H // kqueue helpers
static enum thread_sched_waiting_flag
+490 -560
View File
File diff suppressed because it is too large. Load diff
+4 -14
View File
@@ -8,6 +8,9 @@
Copyright (C) 2004-2007 Koichi Sasada
This platform runs the common scheduler; see thread_sched.h for its data
structures and thread_sched.c for the implementation.
**********************************************************************/
/* interface */
@@ -16,23 +19,10 @@
# undef _WIN32
# endif
#define USE_VM_CLOCK 1
WINBASEAPI BOOL WINAPI
TryEnterCriticalSection(IN OUT LPCRITICAL_SECTION lpCriticalSection);
struct rb_native_thread {
HANDLE thread_id;
HANDLE interrupt_event;
};
struct rb_thread_sched_item {
void *vm_stack;
};
struct rb_thread_sched {
HANDLE lock;
};
#include "thread_sched.h"
typedef DWORD native_tls_key_t; // TLS index
-4
View File
@@ -4903,10 +4903,6 @@ Init_BareVM(void)
vm_opt_method_def_table = st_init_numtable();
vm_opt_mid_table = st_init_numtable();
#ifdef RUBY_THREAD_WIN32_H
rb_native_cond_initialize(&vm->ractor.sync.barrier_complete_cond);
rb_native_cond_initialize(&vm->ractor.sync.barrier_release_cond);
#endif
}
void
+2 -57
View File
@@ -726,69 +726,14 @@ typedef struct rb_vm_struct {
// join at exit
rb_nativethread_cond_t terminate_cond;
bool terminate_waiting;
#ifndef RUBY_THREAD_PTHREAD_H
// win32
bool barrier_waiting;
unsigned int barrier_cnt;
rb_nativethread_cond_t barrier_complete_cond;
rb_nativethread_cond_t barrier_release_cond;
#endif
} sync;
/* VM-wide locks for the Ractor transfer/inheritance machinery. All of them
* are leaf locks: no safepoint inside a critical section. */
rb_nativethread_lock_t generic_fields_lock; /* the shared generic-fields table in variable.c */
#ifdef RUBY_THREAD_PTHREAD_H
// ractor scheduling
struct {
rb_nativethread_lock_t lock;
struct rb_ractor_struct *lock_owner;
bool locked;
rb_nativethread_cond_t cond; // GRQ
rb_atomic_t snt_cnt; // count of shared NTs; lock-free (see native_thread_dedicated_inc)
unsigned int dnt_cnt; // count of dedicated NTs; logging only (USE_RUBY_DEBUG_LOG), not atomic
unsigned int max_cpu;
struct ccan_list_head grq; // // Global Ready Queue
rb_atomic_t winding_cnt; // native threads between a coroutine epilogue and its reclaim; ruby_vm_destruct waits for 0
unsigned int grq_cnt;
// What the barrier walk visits: threads running on dedicated
// nts, and the shared nts (whose running_th fields hold the rest).
struct {
rb_nativethread_lock_t lock;
struct ccan_list_head running_dnts;
struct ccan_list_head snts;
} ntlist;
// scheds whose readyq holds waiters: the timer ticks their
// running thread (timeslice_scan) and prunes drained entries.
struct {
rb_nativethread_lock_t lock;
struct ccan_list_head scheds;
} timeslice;
// true if timeslice timer is not enable
bool timeslice_wait_inf;
// barrier
rb_nativethread_cond_t barrier_complete_cond;
rb_nativethread_cond_t barrier_release_cond;
// bool; nonzero while a stop-the-world section is active. Set
// before the barrier walks the running records; a record moved
// after the walk sees it (thread_sched_setup_running_threads).
rb_atomic_t barrier_is_waiting;
unsigned int barrier_joined_cnt; // threads joined so far; under sched.lock
unsigned int barrier_running_cnt; // runners counted by the barrier's walk; under sched.lock
unsigned int barrier_serial;
struct rb_ractor_struct *barrier_ractor;
unsigned int barrier_lock_rec;
} sched;
#endif
// ractor scheduling; see thread_sched.h
struct rb_ractor_sched sched;
} ractor;
#ifdef USE_SIGALTSTACK
-14
View File
@@ -65,21 +65,13 @@ rb_vm_locked_p(void)
static bool
vm_need_barrier_waiting(const rb_vm_t *vm)
{
#ifdef RUBY_THREAD_PTHREAD_H
return vm->ractor.sched.barrier_is_waiting;
#else
return vm->ractor.sync.barrier_waiting;
#endif
}
static bool
vm_need_barrier(bool no_barrier, const rb_ractor_t *cr, const rb_vm_t *vm)
{
#ifdef RUBY_THREAD_PTHREAD_H
return !no_barrier && cr->threads.sched.running != NULL && vm_need_barrier_waiting(vm); // ractor has running threads.
#else
return !no_barrier && vm_need_barrier_waiting(vm);
#endif
}
static void
@@ -142,13 +134,11 @@ vm_lock_leave(rb_vm_t *vm, bool no_barrier, unsigned int *lev APPEND_LOCATION_AR
VM_ASSERT(vm->ractor.sync.lock_rec == *lev);
VM_ASSERT(cr == GET_RACTOR());
#ifdef RUBY_THREAD_PTHREAD_H
if (vm->ractor.sched.barrier_ractor == cr &&
vm->ractor.sched.barrier_lock_rec == vm->ractor.sync.lock_rec) {
VM_ASSERT(!no_barrier);
rb_ractor_sched_barrier_end(vm, cr);
}
#endif
if (RUBY_DTRACE_GVL_RELEASE_ENABLED()) {
RUBY_DTRACE_GVL_RELEASE();
@@ -258,11 +248,7 @@ rb_vm_cond_timedwait(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long ms
static bool
vm_barrier_acquired_p(const rb_vm_t *vm, const rb_ractor_t *cr)
{
#ifdef RUBY_THREAD_PTHREAD_H
return vm->ractor.sched.barrier_ractor == cr;
#else
return false;
#endif
}
void