Files
ruby/vm_sync.c
T
Koichi SasadaandClaude Opus 5 90e729c9bc 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>
2026-08-29 04:04:48 +09:00

312 lines
7.9 KiB
C

#include "internal/gc.h"
#include "internal/thread.h"
#include "vm_core.h"
#include "vm_sync.h"
#include "ractor_core.h"
#include "vm_debug.h"
#include "probes.h"
void rb_ractor_sched_barrier_start(rb_vm_t *vm, rb_ractor_t *cr);
void rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr);
void rb_ractor_sched_barrier_end(rb_vm_t *vm, rb_ractor_t *cr);
static bool
vm_locked(rb_vm_t *vm)
{
return vm_locked_by_ractor_p(vm, GET_RACTOR());
}
#if RUBY_DEBUG > 0
static bool
vm_lock_assertable_p(void)
{
return rb_current_execution_context(false) != NULL;
}
void
RUBY_ASSERT_vm_locking(void)
{
if (vm_lock_assertable_p() && rb_multi_ractor_p()) {
rb_vm_t *vm = GET_VM();
VM_ASSERT(vm_locked(vm));
}
}
void
RUBY_ASSERT_vm_locking_with_barrier(void)
{
if (vm_lock_assertable_p() && rb_multi_ractor_p()) {
rb_vm_t *vm = GET_VM();
VM_ASSERT(vm_locked(vm));
if (vm->ractor.cnt > 1) {
/* Written to only when holding both ractor.sync and ractor.sched lock */
VM_ASSERT(vm->ractor.sched.barrier_is_waiting);
}
}
}
void
RUBY_ASSERT_vm_unlocking(void)
{
if (vm_lock_assertable_p() && rb_multi_ractor_p()) {
rb_vm_t *vm = GET_VM();
VM_ASSERT(!vm_locked(vm));
}
}
#endif
bool
rb_vm_locked_p(void)
{
return vm_locked(GET_VM());
}
static bool
vm_need_barrier_waiting(const rb_vm_t *vm)
{
return vm->ractor.sched.barrier_is_waiting;
}
static bool
vm_need_barrier(bool no_barrier, const rb_ractor_t *cr, const rb_vm_t *vm)
{
return !no_barrier && cr->threads.sched.running != NULL && vm_need_barrier_waiting(vm); // ractor has running threads.
}
static void
vm_lock_enter(rb_ractor_t *cr, rb_vm_t *vm, bool locked, bool no_barrier, unsigned int *lev APPEND_LOCATION_ARGS)
{
RUBY_DEBUG_LOG2(file, line, "start locked:%d", locked);
if (locked) {
ASSERT_vm_locking();
}
else {
#if RACTOR_CHECK_MODE
// locking ractor and acquire VM lock will cause deadlock
VM_ASSERT(cr->sync.locked_by != rb_ractor_self(cr));
#endif
// lock
rb_native_mutex_lock(&vm->ractor.sync.lock);
VM_ASSERT(vm->ractor.sync.lock_owner == NULL);
VM_ASSERT(vm->ractor.sync.lock_rec == 0);
// barrier
if (vm_need_barrier(no_barrier, cr, vm)) {
rb_execution_context_t *ec = GET_EC();
RB_VM_SAVE_MACHINE_CONTEXT(rb_ec_thread_ptr(ec));
do {
VM_ASSERT(vm_need_barrier_waiting(vm));
RUBY_DEBUG_LOG("barrier serial:%u", vm->ractor.sched.barrier_serial);
rb_ractor_sched_barrier_join(vm, cr);
} while (vm_need_barrier_waiting(vm));
}
VM_ASSERT(vm->ractor.sync.lock_rec == 0);
VM_ASSERT(vm->ractor.sync.lock_owner == NULL);
vm->ractor.sync.lock_owner = cr;
}
vm->ractor.sync.lock_rec++;
*lev = vm->ractor.sync.lock_rec;
RUBY_DEBUG_LOG2(file, line, "rec:%u owner:%u", vm->ractor.sync.lock_rec,
(unsigned int)rb_ractor_id(vm->ractor.sync.lock_owner));
if (RUBY_DTRACE_GVL_ACQUIRE_ENABLED()) {
RUBY_DTRACE_GVL_ACQUIRE();
}
}
static void
vm_lock_leave(rb_vm_t *vm, bool no_barrier, unsigned int *lev APPEND_LOCATION_ARGS)
{
MAYBE_UNUSED(rb_ractor_t *cr = vm->ractor.sync.lock_owner);
RUBY_DEBUG_LOG2(file, line, "rec:%u owner:%u%s", vm->ractor.sync.lock_rec,
(unsigned int)rb_ractor_id(cr),
vm->ractor.sync.lock_rec == 1 ? " (leave)" : "");
ASSERT_vm_locking();
VM_ASSERT(vm->ractor.sync.lock_rec > 0);
VM_ASSERT(vm->ractor.sync.lock_rec == *lev);
VM_ASSERT(cr == GET_RACTOR());
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);
}
if (RUBY_DTRACE_GVL_RELEASE_ENABLED()) {
RUBY_DTRACE_GVL_RELEASE();
}
vm->ractor.sync.lock_rec--;
*lev = vm->ractor.sync.lock_rec;
if (vm->ractor.sync.lock_rec == 0) {
vm->ractor.sync.lock_owner = NULL;
rb_native_mutex_unlock(&vm->ractor.sync.lock);
}
}
void
rb_vm_lock_enter_body(unsigned int *lev APPEND_LOCATION_ARGS)
{
rb_vm_t *vm = GET_VM();
if (vm_locked(vm)) {
vm_lock_enter(NULL, vm, true, false, lev APPEND_LOCATION_PARAMS);
}
else {
vm_lock_enter(GET_RACTOR(), vm, false, false, lev APPEND_LOCATION_PARAMS);
}
}
void
rb_vm_lock_enter_body_nb(unsigned int *lev APPEND_LOCATION_ARGS)
{
rb_vm_t *vm = GET_VM();
if (vm_locked(vm)) {
vm_lock_enter(NULL, vm, true, true, lev APPEND_LOCATION_PARAMS);
}
else {
vm_lock_enter(GET_RACTOR(), vm, false, true, lev APPEND_LOCATION_PARAMS);
}
}
void
rb_vm_lock_enter_body_cr(rb_ractor_t *cr, unsigned int *lev APPEND_LOCATION_ARGS)
{
rb_vm_t *vm = GET_VM();
vm_lock_enter(cr, vm, vm_locked(vm), false, lev APPEND_LOCATION_PARAMS);
}
void
rb_vm_lock_leave_body_nb(unsigned int *lev APPEND_LOCATION_ARGS)
{
vm_lock_leave(GET_VM(), true, lev APPEND_LOCATION_PARAMS);
}
void
rb_vm_lock_leave_body(unsigned int *lev APPEND_LOCATION_ARGS)
{
vm_lock_leave(GET_VM(), false, lev APPEND_LOCATION_PARAMS);
}
void
rb_vm_lock_body(LOCATION_ARGS)
{
rb_vm_t *vm = GET_VM();
ASSERT_vm_unlocking();
vm_lock_enter(GET_RACTOR(), vm, false, false, &vm->ractor.sync.lock_rec APPEND_LOCATION_PARAMS);
}
void
rb_vm_unlock_body(LOCATION_ARGS)
{
rb_vm_t *vm = GET_VM();
ASSERT_vm_locking();
VM_ASSERT(vm->ractor.sync.lock_rec == 1);
vm_lock_leave(vm, false, &vm->ractor.sync.lock_rec APPEND_LOCATION_PARAMS);
}
static void
vm_cond_wait(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
{
ASSERT_vm_locking();
unsigned int lock_rec = vm->ractor.sync.lock_rec;
rb_ractor_t *cr = vm->ractor.sync.lock_owner;
vm->ractor.sync.lock_rec = 0;
vm->ractor.sync.lock_owner = NULL;
if (msec > 0) {
rb_native_cond_timedwait(cond, &vm->ractor.sync.lock, msec);
}
else {
rb_native_cond_wait(cond, &vm->ractor.sync.lock);
}
vm->ractor.sync.lock_rec = lock_rec;
vm->ractor.sync.lock_owner = cr;
}
void
rb_vm_cond_wait(rb_vm_t *vm, rb_nativethread_cond_t *cond)
{
vm_cond_wait(vm, cond, 0);
}
void
rb_vm_cond_timedwait(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
{
vm_cond_wait(vm, cond, msec);
}
static bool
vm_barrier_acquired_p(const rb_vm_t *vm, const rb_ractor_t *cr)
{
return vm->ractor.sched.barrier_ractor == cr;
}
void
rb_vm_barrier(void)
{
RB_DEBUG_COUNTER_INC(vm_sync_barrier);
if (!rb_multi_ractor_p()) {
// no other ractors
return;
}
else {
rb_vm_t *vm = GET_VM();
rb_ractor_t *cr = vm->ractor.sync.lock_owner;
ASSERT_vm_locking();
VM_ASSERT(cr == GET_RACTOR());
VM_ASSERT(rb_ractor_status_p(cr, ractor_running));
if (vm_barrier_acquired_p(vm, cr)) {
// already in barrier synchronization
return;
}
else {
VM_ASSERT(!vm->ractor.sched.barrier_is_waiting);
rb_ractor_sched_barrier_start(vm, cr);
}
}
}
void
rb_ec_vm_lock_rec_release(const rb_execution_context_t *ec,
unsigned int recorded_lock_rec,
unsigned int current_lock_rec)
{
VM_ASSERT(recorded_lock_rec != current_lock_rec);
if (UNLIKELY(recorded_lock_rec > current_lock_rec)) {
rb_bug("unexpected situation - recordd:%u current:%u",
recorded_lock_rec, current_lock_rec);
}
else {
while (recorded_lock_rec < current_lock_rec) {
RB_VM_LOCK_LEAVE_LEV(&current_lock_rec);
}
}
VM_ASSERT(recorded_lock_rec == rb_ec_vm_lock_rec(ec));
}
VALUE
rb_vm_lock_with_barrier(VALUE (*func)(void *args), void *args)
{
VALUE result = 0;
RB_VM_LOCKING() {
rb_vm_barrier();
result = func(args);
}
return result;
}