Ruby 4.1.0dev (2026-09-21 revision 61de3dd727146cfcf24e8051595bb2fb842f37aa)
thread_sched.c (61de3dd727146cfcf24e8051595bb2fb842f37aa)
1/* -*-c-*- */
2/**********************************************************************
3
4 thread_sched.c - platform independent thread/ractor scheduler
5
6 This file is #included from thread.c. It pulls in the platform
7 implementation (THREAD_IMPL_SRC: thread_pthread.c or thread_win32.c)
8 first, then builds the scheduler on top of the primitives that file
9 provides. It implements:
10
11 - the per-Ractor thread scheduler (GVL): struct rb_thread_sched
12 - the Ractor scheduler: global ready queue (grq) and the VM barrier
13 - the native thread (NT) main loop
14 - the timer thread main loop and time slice management
15 - the unblocking function (UBF) list
16
17 See thread_sched.h for the data structures and for the primitives the
18 platform layer has to supply.
19
20**********************************************************************/
21
22/* ------------------------------------------------------------------------
23 * The scheduler <-> platform contract.
24 *
25 * The platform implementation is included below, ahead of the scheduler
26 * body, so it can use every primitive it defines without declaring them.
27 * The traffic in the other direction -- the scheduler entry points the
28 * platform layer (and the M:N scheduler it includes) calls back into --
29 * has to be declared here instead.
30 * ------------------------------------------------------------------------ */
31
32#define thread_sched_dump(s) thread_sched_dump_(__FILE__, __LINE__, s)
33#define ractor_sched_dump(s) ractor_sched_dump_(__FILE__, __LINE__, s)
34
35#define thread_sched_lock(a, b) thread_sched_lock_(a, b, __FILE__, __LINE__)
36#define thread_sched_unlock(a, b) thread_sched_unlock_(a, b, __FILE__, __LINE__)
37#define ractor_sched_lock(a, b) ractor_sched_lock_(a, b, __FILE__, __LINE__)
38#define ractor_sched_unlock(a, b) ractor_sched_unlock_(a, b, __FILE__, __LINE__)
39
40#ifndef MINIMUM_SNT
41// make at least MINIMUM_SNT snts for debug.
42#define MINIMUM_SNT 0
43#endif
44
46
47#include "probes.h"
48
49// thread.c
50static void threadptr_trap_interrupt(rb_thread_t *);
51
52// thread scheduler (GVL)
53static void thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th);
54static void thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately);
55static void thread_sched_switch(rb_thread_t *cth, rb_thread_t *next_th);
56static void coroutine_transfer0(struct coroutine_context *transfer_from,
57 struct coroutine_context *transfer_to, bool to_dead);
58static void thread_sched_lock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *file, int line);
59static void thread_sched_unlock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *file, int line);
60static void thread_sched_unlock_no_log(struct rb_thread_sched *sched, rb_thread_t *th);
61static void thread_sched_set_locked(struct rb_thread_sched *sched, rb_thread_t *th);
62static void thread_sched_setup_running_threads(struct rb_thread_sched *sched, rb_ractor_t *cr, rb_vm_t *vm,
63 rb_thread_t *add_th, rb_thread_t *del_th);
64static void thread_sched_add_running_thread(struct rb_thread_sched *sched, rb_thread_t *th);
65static void thread_sched_to_ready(struct rb_thread_sched *sched, rb_thread_t *th);
66static void thread_sched_to_ready_common(struct rb_thread_sched *sched, rb_thread_t *th, bool wakeup, bool will_switch);
67static void thread_sched_to_dead_common(struct rb_thread_sched *sched, rb_thread_t *th);
68static void thread_sched_to_waiting_until_wakeup(struct rb_thread_sched *sched, rb_thread_t *th, const rb_hrtime_t *end);
69static void thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, bool can_direct_transfer, const rb_hrtime_t *end);
70static void thread_sched_wakeup_next_thread(struct rb_thread_sched *sched, rb_thread_t *th, bool will_switch);
71
72// native thread <-> ractor assignment
73static void native_thread_dedicated_inc(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_thread *nt);
74static void native_thread_dedicated_dec(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_thread *nt);
75static void native_thread_assign(struct rb_native_thread *nt, rb_thread_t *th);
76
77// ractor scheduler
78static void ractor_sched_lock_(rb_vm_t *vm, rb_ractor_t *cr, const char *file, int line);
79static void ractor_sched_unlock_(rb_vm_t *vm, rb_ractor_t *cr, const char *file, int line);
80static void ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r);
81static void ractor_sched_cancel_enq(rb_vm_t *vm, struct rb_thread_sched *sched);
82
83// VM wide scheduler state; the platform's Init_native_thread() calls this
84static void thread_sched_init_vm(rb_vm_t *vm);
85
86// implemented by the M:N scheduler (thread_sched_mn.c) or stubbed out
87static bool ractor_sched_timeout_arm(rb_thread_t *th, const rb_hrtime_t *rel);
88static bool ractor_sched_timeout_disarm(rb_thread_t *th);
89static void timer_thread_wake_fence(struct rb_thread_struct *th);
90
91// unblocking function (UBF)
92static bool ubf_set(rb_thread_t *th, rb_unblock_function_t *func, void *arg, rb_atomic_t *event_serial);
93static void ubf_clear(rb_thread_t *th, bool clear_serial);
94
95// native thread main loops
96static void call_thread_start_func_2(rb_thread_t *th);
97static void *nt_start(void *ptr);
98
99// timer thread
100static void *timer_thread_func(void *ptr);
101static int timer_thread_set_timeout(rb_vm_t *vm);
102static void timer_thread_check_timeslice(rb_vm_t *vm);
103static bool timeslice_scan(rb_vm_t *vm, bool interrupt);
104static void timer_thread_wakeup(void);
105static void timer_thread_wakeup_locked(rb_vm_t *vm);
106static void timer_thread_wakeup_force(void);
107// RUBY_MN_THREADS: -1 = nothing is M:N, not even a Ractor's threads;
108// 0 = the default (a Ractor's threads are, the main Ractor's are not);
109// 1 = the main Ractor's threads too; 2 = the main thread as well.
110static int mn_threads_mode = 0;
111
112static void nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt);
113static void nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt);
114static bool nt_shared_loop(struct rb_native_thread *nt);
115static bool native_thread_self_can_retire_p(void);
116
117#include THREAD_IMPL_SRC
118
119#if USE_MN_THREADS
120static void thread_sched_main_to_shared(rb_thread_t *th);
121#endif
122
123// Defaults for what the platform above did not opt out of.
124
125#ifndef RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF
126// Whether rb_native_mutex_trylock() reports EBUSY when the calling thread is
127// itself the owner. A recursive lock (a Windows CRITICAL_SECTION) grants it
128// again instead, so it cannot back a "somebody holds this" assertion.
129#define RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF 1
130#endif
131
132/* ------------------------------------------------------------------------
133 * The scheduler itself.
134 * ------------------------------------------------------------------------ */
135
136static bool
137th_has_dedicated_nt(const rb_thread_t *th)
138{
139 // TODO: th->has_dedicated_nt
140 return th->nt->dedicated > 0;
141}
142
144static void
145thread_sched_dump_(const char *file, int line, struct rb_thread_sched *sched)
146{
147 fprintf(stderr, "@%s:%d running:%d\n", file, line, sched->running ? (int)sched->running->serial : -1);
148 rb_thread_t *th;
149 int i = 0;
150 ccan_list_for_each(&sched->readyq, th, sched.node.readyq) {
151 i++; if (i>10) rb_bug("too many");
152 fprintf(stderr, " ready:%d (%sNT:%d)\n", th->serial,
153 th->nt ? (th->nt->dedicated ? "D" : "S") : "x",
154 th->nt ? (int)th->nt->serial : -1);
155 }
156}
157
158
160static void
161ractor_sched_dump_(const char *file, int line, rb_vm_t *vm)
162{
163 rb_ractor_t *r;
164
165 fprintf(stderr, "ractor_sched_dump %s:%d\n", file, line);
166
167 int i = 0;
168 ccan_list_for_each(&vm->ractor.sched.grq, r, threads.sched.grq_node) {
169 i++;
170 if (i>10) rb_bug("!!");
171 fprintf(stderr, " %d ready:%"PRI_SERIALT_PREFIX"u\n", i, rb_ractor_id(r));
172 }
173}
174
175
176static void
177thread_sched_set_locked(struct rb_thread_sched *sched, rb_thread_t *th)
178{
179#if VM_CHECK_MODE > 0
180 VM_ASSERT(sched->lock_owner == NULL);
181
182 sched->lock_owner = th;
183#endif
184}
185
186static void
187thread_sched_set_unlocked(struct rb_thread_sched *sched, rb_thread_t *th)
188{
189#if VM_CHECK_MODE > 0
190 VM_ASSERT(sched->lock_owner == th);
191
192 sched->lock_owner = NULL;
193#endif
194}
195
196static void
197thread_sched_lock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *file, int line)
198{
199 rb_native_mutex_lock(&sched->lock_);
200
201#if VM_CHECK_MODE
202 RUBY_DEBUG_LOG2(file, line, "r:%d th:%u", th ? (int)rb_ractor_id(th->ractor) : -1, rb_th_serial(th));
203#else
204 RUBY_DEBUG_LOG2(file, line, "th:%u", rb_th_serial(th));
205#endif
206
207 thread_sched_set_locked(sched, th);
208}
209
210static void
211thread_sched_unlock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *file, int line)
212{
213 RUBY_DEBUG_LOG2(file, line, "th:%u", rb_th_serial(th));
214
215 thread_sched_set_unlocked(sched, th);
216
217 rb_native_mutex_unlock(&sched->lock_);
218}
219
220// Like thread_sched_unlock(), but never dereferences th (the debug log above
221// reads th->serial). For the MN termination epilogue, which unlocks after th
222// may already be collectable. Keep in sync with thread_sched_unlock_.
224static void
225thread_sched_unlock_no_log(struct rb_thread_sched *sched, rb_thread_t *th)
226{
227 thread_sched_set_unlocked(sched, th); // pointer compare only
228
229 rb_native_mutex_unlock(&sched->lock_);
230}
231
232static void
233ASSERT_thread_sched_locked(struct rb_thread_sched *sched, rb_thread_t *th)
234{
235#if RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF
236 VM_ASSERT(rb_native_mutex_trylock(&sched->lock_) == EBUSY);
237#endif
238
239#if VM_CHECK_MODE
240 if (th) {
241 VM_ASSERT(sched->lock_owner == th);
242 }
243 else {
244 VM_ASSERT(sched->lock_owner != NULL);
245 }
246#endif
247}
248
249
251static rb_serial_t
252rb_ractor_serial(const rb_ractor_t *r)
253{
254 if (r) {
255 return rb_ractor_id(r);
256 }
257 else {
258 return 0;
259 }
260}
261
262static void
263ractor_sched_set_locked(rb_vm_t *vm, rb_ractor_t *cr)
264{
265#if VM_CHECK_MODE > 0
266 VM_ASSERT(vm->ractor.sched.lock_owner == NULL);
267 VM_ASSERT(vm->ractor.sched.locked == false);
268
269 vm->ractor.sched.lock_owner = cr;
270 vm->ractor.sched.locked = true;
271#endif
272}
273
274static void
275ractor_sched_set_unlocked(rb_vm_t *vm, rb_ractor_t *cr)
276{
277#if VM_CHECK_MODE > 0
278 VM_ASSERT(vm->ractor.sched.locked);
279 VM_ASSERT(vm->ractor.sched.lock_owner == cr);
280
281 vm->ractor.sched.locked = false;
282 vm->ractor.sched.lock_owner = NULL;
283#endif
284}
285
286
287static void
288ractor_sched_lock_(rb_vm_t *vm, rb_ractor_t *cr, const char *file, int line)
289{
290 rb_native_mutex_lock(&vm->ractor.sched.lock);
291
292#if VM_CHECK_MODE
293 RUBY_DEBUG_LOG2(file, line, "cr:%"PRI_SERIALT_PREFIX"u prev_owner:%"PRI_SERIALT_PREFIX"u", rb_ractor_serial(cr), rb_ractor_serial(vm->ractor.sched.lock_owner));
294#else
295 RUBY_DEBUG_LOG2(file, line, "cr:%"PRI_SERIALT_PREFIX"u", rb_ractor_serial(cr));
296#endif
297
298 ractor_sched_set_locked(vm, cr);
299}
300
301static void
302ractor_sched_unlock_(rb_vm_t *vm, rb_ractor_t *cr, const char *file, int line)
303{
304 RUBY_DEBUG_LOG2(file, line, "cr:%"PRI_SERIALT_PREFIX"u", rb_ractor_serial(cr));
305
306 ractor_sched_set_unlocked(vm, cr);
307 rb_native_mutex_unlock(&vm->ractor.sched.lock);
308}
309
310static void
311ASSERT_ractor_sched_locked(rb_vm_t *vm, rb_ractor_t *cr)
312{
313#if RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF
314 VM_ASSERT(rb_native_mutex_trylock(&vm->ractor.sched.lock) == EBUSY);
315#endif
316 VM_ASSERT(vm->ractor.sched.locked);
317 VM_ASSERT(cr == NULL || vm->ractor.sched.lock_owner == cr);
318}
319
320static void ractor_sched_barrier_join_signal_locked(rb_vm_t *vm);
321
322/* ntlist registration: a thread that executes Ruby code is always registered,
323 * in its snt's nt->running_th or on running_dnts via its dedicated nt. The
324 * only unregistered execution is scheduler glue (parking, resuming), which
325 * touches no Ruby heap, and the barrier wait below. */
326static void
327ntlist_add_running(rb_vm_t *vm, rb_thread_t *th)
328{
329 struct rb_native_thread *nt = th->nt;
330
331 // a dedicated nt is not on the snts list the scans walk: running_dnts instead
332 if (nt != NULL && nt->dedicated == 0) {
333 rb_native_mutex_lock(&nt->running_th_lock);
334 {
335 VM_ASSERT(nt->running_th == NULL);
336 nt->running_th = th;
337 }
338 rb_native_mutex_unlock(&nt->running_th_lock);
339 }
340 else {
341 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
342 {
343 // an snt gone dedicated (rb_thread_lock_native_thread) has no
344 // creation-time running_thread: the registration supplies it
345 nt->running_thread = th;
346 ccan_list_add(&vm->ractor.sched.ntlist.running_dnts, &nt->running_dnts_node);
347 }
348 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
349 }
350}
351
352// Returns whether the active barrier's walk had counted this registration:
353// such a deregistration owes the snapshot count a decrement. Read and
354// cleared under the registration's own lock, so it pairs with the walk.
355static bool
356ntlist_del_running(rb_vm_t *vm, rb_thread_t *th)
357{
358 struct rb_native_thread *nt = th->nt;
359 uint32_t serial;
360 bool counted;
361 bool in_running_th;
362
363 // The registration itself says where it is: nt->running_th holds th, or
364 // th's nt hangs on running_dnts. barrier_serial is read inside the
365 // registration's lock, ordered with the walk that stamped there.
366 rb_native_mutex_lock(&nt->running_th_lock);
367 {
368 in_running_th = (nt->running_th == th);
369 if (in_running_th) {
370 nt->running_th = NULL;
371 serial = vm->ractor.sched.barrier_serial;
372 counted = (nt->barrier_counted_serial == serial);
373 nt->barrier_counted_serial = serial - 1; // only once per barrier
374 }
375 }
376 rb_native_mutex_unlock(&nt->running_th_lock);
377
378 if (!in_running_th) {
379 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
380 {
381 ccan_list_del_init(&nt->running_dnts_node);
382 serial = vm->ractor.sched.barrier_serial;
383 counted = (nt->barrier_counted_serial == serial);
384 nt->barrier_counted_serial = serial - 1;
385 }
386 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
387 }
388 return counted;
389}
390
391// Stamp a registration into the active barrier's snapshot unless the walk
392// already counted it; returns whether it stamped. Called under sched.lock,
393// so it is serialized with the walk: the stamp says exactly whether the
394// registration came first.
395static bool
396ntlist_stamp_if_uncounted(rb_vm_t *vm, rb_thread_t *th)
397{
398 struct rb_native_thread *nt = th->nt;
399 uint32_t serial = vm->ractor.sched.barrier_serial; // sched.lock is held
400 bool stamped;
401 bool in_running_th;
402
403 rb_native_mutex_lock(&nt->running_th_lock);
404 {
405 in_running_th = (nt->running_th == th);
406 if (in_running_th) {
407 stamped = (nt->barrier_counted_serial != serial);
408 nt->barrier_counted_serial = serial;
409 }
410 }
411 rb_native_mutex_unlock(&nt->running_th_lock);
412
413 if (!in_running_th) {
414 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
415 {
416 stamped = (nt->barrier_counted_serial != serial);
417 nt->barrier_counted_serial = serial;
418 }
419 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
420 }
421 return stamped;
422}
423
424// Record a thread entering/leaving the running set, with no global lock and
425// no count: the records themselves are what the barrier counts. Pairing:
426// the barrier sets barrier_is_waiting and then walks the records under their
427// locks; we move a record and then read the flag, so one side sees the other.
428// List sched for the timer's timeslice ticks. The caller holds sched->lock_
429// with the readyq non-empty, so the timer cannot prune the entry meanwhile.
430static void
431timeslice_sched_link(rb_vm_t *vm, struct rb_thread_sched *sched)
432{
433 rb_native_mutex_lock(&vm->ractor.sched.timeslice.lock);
434 {
435 if (sched->timeslice_node.next == &sched->timeslice_node) {
436 ccan_list_add_tail(&vm->ractor.sched.timeslice.scheds, &sched->timeslice_node);
437 }
438 }
439 rb_native_mutex_unlock(&vm->ractor.sched.timeslice.lock);
440}
441
442static void
443thread_sched_setup_running_threads(struct rb_thread_sched *sched, rb_ractor_t *cr, rb_vm_t *vm,
444 rb_thread_t *add_th, rb_thread_t *del_th)
445{
446 RUBY_DEBUG_LOG("+:%u -:%u", rb_th_serial(add_th), rb_th_serial(del_th));
447
448 if (del_th) {
449 bool counted = ntlist_del_running(vm, del_th);
450 sched->is_running = false;
451
452 // The first load is only a filter; the one under sched.lock decides.
453 // A missed flag means this deregistration preceded the barrier's walk.
454 if (UNLIKELY(RUBY_ATOMIC_LOAD(vm->ractor.sched.barrier_is_waiting))) {
455 ractor_sched_lock(vm, cr);
456 {
457 if (RUBY_ATOMIC_LOAD(vm->ractor.sched.barrier_is_waiting)) {
458 if (counted) {
459 VM_ASSERT(vm->ractor.sched.barrier_running_cnt > 0);
460 vm->ractor.sched.barrier_running_cnt--;
461 }
462 ractor_sched_barrier_join_signal_locked(vm);
463 }
464 }
465 ractor_sched_unlock(vm, cr);
466 }
467 }
468
469 if (add_th) {
470 ntlist_add_running(vm, add_th);
471
472 if (UNLIKELY(RUBY_ATOMIC_LOAD(vm->ractor.sched.barrier_is_waiting))) {
473 // A stop-the-world section. In its waiting phase sched.lock is
474 // takable: join the snapshot count and take the interrupt (this
475 // thread joins at its next check, like any walked runner). In
476 // the GC phase the barrier holds sched.lock to its end, so this
477 // blocks here, as the old global-lock design did.
478 ractor_sched_lock(vm, cr);
479 {
480 if (RUBY_ATOMIC_LOAD(vm->ractor.sched.barrier_is_waiting) &&
481 ntlist_stamp_if_uncounted(vm, add_th)) {
482 // the walk ran before this registration; count it in
483 RUBY_DEBUG_LOG("barrier_is_waiting");
484 vm->ractor.sched.barrier_running_cnt++;
485 RUBY_VM_SET_VM_BARRIER_INTERRUPT(add_th->ec);
486 }
487 }
488 ractor_sched_unlock(vm, cr);
489 }
490
491 sched->is_running = true;
492
493 // taking a turn with waiters already queued needs the timeslice ticks
494 if (!ccan_list_empty(&sched->readyq)) {
495 timeslice_sched_link(vm, sched);
496 ractor_sched_lock(vm, cr);
497 {
498 if (vm->ractor.sched.timeslice_wait_inf) {
499 timer_thread_wakeup_locked(vm);
500 }
501 }
502 ractor_sched_unlock(vm, cr);
503 }
504 }
505}
506
507static void
508thread_sched_add_running_thread(struct rb_thread_sched *sched, rb_thread_t *th)
509{
510 ASSERT_thread_sched_locked(sched, th);
511 VM_ASSERT(sched->running == th);
512
513 rb_vm_t *vm = th->vm;
514 thread_sched_setup_running_threads(sched, th->ractor, vm, th, NULL);
515}
516
517static void
518thread_sched_del_running_thread(struct rb_thread_sched *sched, rb_thread_t *th)
519{
520 ASSERT_thread_sched_locked(sched, th);
521
522 rb_vm_t *vm = th->vm;
523 thread_sched_setup_running_threads(sched, th->ractor, vm, NULL, th);
524}
525
526void
527rb_add_running_thread(rb_thread_t *th)
528{
529 struct rb_thread_sched *sched = TH_SCHED(th);
530
531 thread_sched_lock(sched, th);
532 {
533 thread_sched_add_running_thread(sched, th);
534 }
535 thread_sched_unlock(sched, th);
536}
537
538void
539rb_del_running_thread(rb_thread_t *th)
540{
541 struct rb_thread_sched *sched = TH_SCHED(th);
542
543 thread_sched_lock(sched, th);
544 {
545 thread_sched_del_running_thread(sched, th);
546 }
547 thread_sched_unlock(sched, th);
548}
549
550// setup current or next running thread
551// sched->running should be set only on this function.
552//
553// if th is NULL, there is no running threads.
554static void
555thread_sched_set_running(struct rb_thread_sched *sched, rb_thread_t *th)
556{
557 RUBY_DEBUG_LOG("th:%u->th:%u", rb_th_serial(sched->running), rb_th_serial(th));
558 VM_ASSERT(sched->running != th);
559
560 if (RUBY_DTRACE_RTS_SET_RUNNING_ENABLED()) {
561 RUBY_DTRACE_RTS_SET_RUNNING(sched, sched->running, th);
562 }
563
564 sched->running = th;
565}
566
568static bool
569thread_sched_readyq_contain_p(struct rb_thread_sched *sched, rb_thread_t *th)
570{
571 rb_thread_t *rth;
572 ccan_list_for_each(&sched->readyq, rth, sched.node.readyq) {
573 if (rth == th) {
574 VM_ASSERT(th->sched.node.is_ready);
575 return true;
576 }
577 }
578 VM_ASSERT(!th->sched.node.is_ready);
579 return false;
580}
581
582// deque thread from the ready queue.
583// if the ready queue is empty, return NULL.
584//
585// return deque'ed running thread (or NULL).
586static rb_thread_t *
587thread_sched_deq(struct rb_thread_sched *sched)
588{
589 ASSERT_thread_sched_locked(sched, NULL);
590 rb_thread_t *next_th;
591
592 VM_ASSERT(sched->running != NULL);
593
594 if (ccan_list_empty(&sched->readyq)) {
595 next_th = NULL;
596 }
597 else {
598 next_th = ccan_list_pop(&sched->readyq, rb_thread_t, sched.node.readyq);
599 VM_ASSERT(next_th->sched.node.is_ready);
600 next_th->sched.node.is_ready = false;
601
602 VM_ASSERT(sched->readyq_cnt > 0);
603 sched->readyq_cnt--;
604 ccan_list_node_init(&next_th->sched.node.readyq);
605 }
606
607 RUBY_DEBUG_LOG("next_th:%u readyq_cnt:%d", rb_th_serial(next_th), sched->readyq_cnt);
608
609 return next_th;
610}
611
612// enqueue ready thread to the ready queue.
613static void
614thread_sched_enq(struct rb_thread_sched *sched, rb_thread_t *ready_th)
615{
616 ASSERT_thread_sched_locked(sched, NULL);
617 RUBY_DEBUG_LOG("ready_th:%u readyq_cnt:%d", rb_th_serial(ready_th), sched->readyq_cnt);
618
619 VM_ASSERT(sched->running != NULL);
620 VM_ASSERT(!thread_sched_readyq_contain_p(sched, ready_th));
621
622 bool timeslice_onset = sched->is_running && ccan_list_empty(&sched->readyq);
623
624 ccan_list_add_tail(&sched->readyq, &ready_th->sched.node.readyq);
625 ready_th->sched.node.is_ready = true;
626 sched->readyq_cnt++;
627
628 if (timeslice_onset) {
629 // The running thread needs the timeslice ticks now. Linked before
630 // the check under sched.lock: either the timer's scan (same lock)
631 // sees the sched, or this sees timeslice_wait_inf.
632 rb_vm_t *vm = ready_th->vm;
633 timeslice_sched_link(vm, sched);
634 ractor_sched_lock(vm, NULL);
635 {
636 if (vm->ractor.sched.timeslice_wait_inf) {
637 timer_thread_wakeup_locked(vm);
638 }
639 }
640 ractor_sched_unlock(vm, NULL);
641 }
642}
643
644// DNT: kick condvar
645// SNT: TODO
646static void
647thread_sched_wakeup_running_thread(struct rb_thread_sched *sched, rb_thread_t *next_th, bool will_switch)
648{
649 ASSERT_thread_sched_locked(sched, NULL);
650 VM_ASSERT(sched->running == next_th);
651
652 if (next_th) {
653 if (next_th->nt) {
654 if (th_has_dedicated_nt(next_th)) {
655 RUBY_DEBUG_LOG("pinning th:%u", next_th->serial);
656 rb_native_cond_signal(&next_th->nt->readyq);
657 }
658 else {
659 // TODO
660 RUBY_DEBUG_LOG("th:%u is already running.", next_th->serial);
661 }
662 }
663 else {
664 if (will_switch) {
665 RUBY_DEBUG_LOG("th:%u (do nothing)", rb_th_serial(next_th));
666 }
667 else {
668 RUBY_DEBUG_LOG("th:%u (enq)", rb_th_serial(next_th));
669 ractor_sched_enq(next_th->vm, next_th->ractor);
670 }
671 }
672 }
673 else {
674 RUBY_DEBUG_LOG("no waiting threads%s", "");
675 }
676}
677
678// waiting -> ready (locked)
679static void
680thread_sched_to_ready_common(struct rb_thread_sched *sched, rb_thread_t *th, bool wakeup, bool will_switch)
681{
682 RUBY_DEBUG_LOG("th:%u running:%u redyq_cnt:%d", rb_th_serial(th), rb_th_serial(sched->running), sched->readyq_cnt);
683
684 VM_ASSERT(sched->running != th);
685 VM_ASSERT(!thread_sched_readyq_contain_p(sched, th));
686 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_READY, th);
687
688 if (sched->running == NULL) {
689 thread_sched_set_running(sched, th);
690 if (wakeup) thread_sched_wakeup_running_thread(sched, th, will_switch);
691 }
692 else {
693 thread_sched_enq(sched, th);
694 }
695}
696
697// waiting -> ready
698//
699// `th` had became "waiting" state by `thread_sched_to_waiting`
700// and `thread_sched_to_ready` enqueue `th` to the thread ready queue.
702static void
703thread_sched_to_ready(struct rb_thread_sched *sched, rb_thread_t *th)
704{
705 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
706
707 thread_sched_lock(sched, th);
708 {
709 thread_sched_to_ready_common(sched, th, true, false);
710 }
711 thread_sched_unlock(sched, th);
712}
713
714// wait until sched->running is `th`. `end` is an absolute deadline for a dedicated
715static void
716thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, bool can_direct_transfer, const rb_hrtime_t *end)
717{
718 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
719
720 ASSERT_thread_sched_locked(sched, th);
721 VM_ASSERT(th == rb_ec_thread_ptr(rb_current_ec_noinline()));
722
723 bool timedout = false;
724
725 if (th != sched->running) {
726 // TODO: This optimization should also be made to work for MN_THREADS
727 if (th->has_dedicated_nt && th == sched->runnable_hot_th && (sched->running == NULL || sched->running->has_dedicated_nt)) {
728 RUBY_DEBUG_LOG("(nt) stealing: hot-th:%u. running:%u", rb_th_serial(th), rb_th_serial(sched->running));
729
730 // th serves itself on its own nt, displacing the enqueued
731 // running thread back to the readyq: cancel the entry that was
732 // posted for it (a later dequeue would find this Ractor served
733 // and its next enqueue would double-list the node)
734 ractor_sched_cancel_enq(th->vm, sched);
735
736 // If there is a thread set to run, move it back to the front of the readyq
737 if (sched->running != NULL) {
738 rb_thread_t *running = sched->running;
739 VM_ASSERT(!thread_sched_readyq_contain_p(sched, running));
740 running->sched.node.is_ready = true;
741 ccan_list_add(&sched->readyq, &running->sched.node.readyq);
742 sched->readyq_cnt++;
743 }
744
745 // Pull off the ready queue and start running.
746 if (th->sched.node.is_ready) {
747 VM_ASSERT(thread_sched_readyq_contain_p(sched, th));
748 ccan_list_del_init(&th->sched.node.readyq);
749 th->sched.node.is_ready = false;
750 sched->readyq_cnt--;
751 }
752 thread_sched_set_running(sched, th);
753 rb_ractor_thread_switch(th->ractor, th, false);
754 }
755 else if (th == sched->runnable_hot_th) {
756 // The hot thread cannot steal the control (e.g. the running thread
757 // is an MN thread). It is going to sleep, so it is no longer spinning;
758 // drop the hint so that other threads don't yield the lock to it.
759 sched->runnable_hot_th = NULL;
760 sched->runnable_hot_th_waiting = 0;
761 }
762
763 // already deleted from running threads
764
765
766 // wait for execution right
767 rb_thread_t *next_th;
768 while((next_th = sched->running) != th) {
769 if (th_has_dedicated_nt(th)) {
770 RUBY_DEBUG_LOG("(nt) sleep th:%u running:%u", rb_th_serial(th), rb_th_serial(sched->running));
771
772 thread_sched_set_unlocked(sched, th);
773 {
774 RUBY_DEBUG_LOG("nt:%d cond:%p", th->nt->serial, &th->nt->readyq);
775 rb_nativethread_cond_t *cond = &th->nt->readyq;
776
777 // Once someone has queued this thread the deadline is spent: it
778 // is waiting for a turn, not for the time, and arming a kernel
779 // timer for every round of that costs more than the wait.
780 // Once someone has queued this thread the deadline is spent: it
781 // is waiting for a turn, not for the time, and arming a kernel
782 // timer for every round of that costs more than the wait.
783 if (end && !th->sched.node.is_ready) {
784 rb_hrtime_t abs = *end;
785
786 if (!RB_NATIVE_COND_HRTIME_DEADLINE_P()) {
787 // the condvar counts in another clock: restate it there
788 rb_hrtime_t now = rb_hrtime_now();
789 abs = native_cond_timeout(cond, *end > now ? *end - now : 0);
790 }
791 timedout = native_cond_timedwait(cond, &sched->lock_, &abs) == ETIMEDOUT;
792 }
793 else {
794 rb_native_cond_wait(cond, &sched->lock_);
795 }
796 }
797 thread_sched_set_locked(sched, th);
798
799 if (timedout &&
800 sched->running != th && !th->sched.node.is_ready) {
801 // the deadline passed and nobody woke this thread: get back in
802 // line for the running turn, then wait for it without a deadline
803 thread_sched_to_ready_common(sched, th, false, false);
804 end = NULL;
805 }
806
807 if (sched->runnable_hot_th != NULL && sched->runnable_hot_th_waiting) {
808 VM_ASSERT(sched->runnable_hot_th != th);
809 // Give the hot thread a chance to preempt, if it's actively spinning.
810 // On multicore, this reduces the rate of core-switching. On single-core it
811 // should mostly be a nop, since the other thread can't be concurrently spinning.
812 thread_sched_unlock(sched, th);
813 thread_sched_lock(sched, th);
814 }
815
816 RUBY_DEBUG_LOG("(nt) wakeup %s", sched->running == th ? "success" : "failed");
817 if (th == sched->running) {
818 rb_ractor_thread_switch(th->ractor, th, false);
819 }
820 }
821 else {
822 // search another ready thread
823 if (can_direct_transfer &&
824 (next_th = sched->running) != NULL &&
825 !next_th->nt // next_th is running or has dedicated nt
826 ) {
827
828 RUBY_DEBUG_LOG("th:%u->%u (direct)", rb_th_serial(th), rb_th_serial(next_th));
829
830 thread_sched_set_unlocked(sched, th);
831 {
832 rb_ractor_set_current_ec(th->ractor, NULL);
833 thread_sched_switch(th, next_th);
834 }
835 thread_sched_set_locked(sched, th);
836 }
837 else {
838 // search another ready ractor
839 struct rb_native_thread *nt = th->nt;
840 native_thread_assign(NULL, th);
841
842 RUBY_DEBUG_LOG("th:%u->%u (ractor scheduling)", rb_th_serial(th), rb_th_serial(next_th));
843
844 thread_sched_set_unlocked(sched, th);
845 {
846 rb_ractor_set_current_ec(th->ractor, NULL);
847 coroutine_transfer0(th->sched.context, nt->nt_context, false);
848 }
849 thread_sched_set_locked(sched, th);
850 }
851
852 VM_ASSERT(rb_current_ec_noinline() == th->ec);
853 }
854 }
855
856 VM_ASSERT(th->nt != NULL);
857 VM_ASSERT(rb_current_ec_noinline() == th->ec);
858 VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
859
860 // add th to running threads
861 thread_sched_add_running_thread(sched, th);
862 }
863
864 // Control transfer to the current thread is now complete. The original thread
865 // cannot steal control at this point.
866 sched->runnable_hot_th = NULL;
867 sched->runnable_hot_th_waiting = 0;
868
869
870 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_RESUMED, th);
871}
872
873// waiting -> ready -> running (locked)
874static void
875thread_sched_to_running_common(struct rb_thread_sched *sched, rb_thread_t *th)
876{
877 RUBY_DEBUG_LOG("th:%u dedicated:%d", rb_th_serial(th), th_has_dedicated_nt(th));
878
879 VM_ASSERT(sched->running != th);
880 VM_ASSERT(th_has_dedicated_nt(th));
881 VM_ASSERT(GET_THREAD() == th);
882
883 native_thread_dedicated_dec(th->vm, th->ractor, th->nt);
884
885 // waiting -> ready
886 thread_sched_to_ready_common(sched, th, false, false);
887
888 if (sched->running == th) {
889 thread_sched_add_running_thread(sched, th);
890 }
891
892 // TODO: check SNT number
893 thread_sched_wait_running_turn(sched, th, false, NULL);
894}
895
896// waiting -> ready -> running
897//
898// `th` had been waiting by `thread_sched_to_waiting()`
899// and run a dedicated task (like waitpid and so on).
900// After the dedicated task, this function is called
901// to join a normal thread-scheduling.
902static void
903thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th)
904{
905 // We are reading and writing these sched fields without lock cover, but
906 // there are no correctness issues resulting from stale cache or delayed writeback.
907 // When it works, this causes the next-scheduled thread to yield the sched lock
908 // briefly so that we can grab it if we're still spinning (not descheduled yet).
909 if (sched->runnable_hot_th == th) {
910 sched->runnable_hot_th_waiting = 1;
911 }
912 thread_sched_lock(sched, th);
913 {
914 thread_sched_to_running_common(sched, th);
915 }
916 thread_sched_unlock(sched, th);
917}
918
919// resume a next thread in the thread ready queue.
920//
921// deque next running thread from the ready thread queue and
922// resume this thread if available.
923//
924// If the next therad has a dedicated native thraed, simply signal to resume.
925// Otherwise, make the ractor ready and other nt will run the ractor and the thread.
926static void
927thread_sched_wakeup_next_thread(struct rb_thread_sched *sched, rb_thread_t *th, bool will_switch)
928{
929 ASSERT_thread_sched_locked(sched, th);
930
931 VM_ASSERT(sched->running == th);
932 VM_ASSERT(sched->running->nt != NULL);
933
934 rb_thread_t *next_th = thread_sched_deq(sched);
935
936 RUBY_DEBUG_LOG("next_th:%u", rb_th_serial(next_th));
937 VM_ASSERT(th != next_th);
938
939 thread_sched_set_running(sched, next_th);
940 VM_ASSERT(next_th == sched->running);
941 thread_sched_wakeup_running_thread(sched, next_th, will_switch);
942
943 if (th != next_th) {
944 thread_sched_del_running_thread(sched, th);
945 }
946}
947
948// running -> dead (locked)
949static void
950thread_sched_to_dead_common(struct rb_thread_sched *sched, rb_thread_t *th)
951{
952 RUBY_DEBUG_LOG("th:%u DNT:%d", rb_th_serial(th), th->nt->dedicated);
953
954 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
955
956 // A dying coroutine thread (will_switch=true here) does NOT wake the
957 // next thread now: it is still winding down (co_start's epilogue), and
958 // the same Ractor must not have two threads executing at once. The
959 // epilogue enqueues the Ractor after its last rb_ractor_t access.
960 thread_sched_wakeup_next_thread(sched, th, !th_has_dedicated_nt(th));
961
962 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_EXITED, th);
963}
964
965// running -> dead
966static void
967thread_sched_to_dead(struct rb_thread_sched *sched, rb_thread_t *th)
968{
969 // wait out any pending wake here, while th's Ractor is still alive
970 timer_thread_wake_fence(th);
971
972 thread_sched_lock(sched, th);
973 {
974 thread_sched_to_dead_common(sched, th);
975 }
976 thread_sched_unlock(sched, th);
977}
978
979// running -> waiting (locked)
980//
981// This thread will run dedicated task (th->nt->dedicated++).
982static void
983thread_sched_to_waiting_common(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately)
984{
985 RUBY_DEBUG_LOG("th:%u DNT:%d", rb_th_serial(th), th->nt->dedicated);
986
987 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
988
989 native_thread_dedicated_inc(th->vm, th->ractor, th->nt);
990 if (!yield_immediately) {
991 sched->runnable_hot_th = th;
992 sched->runnable_hot_th_waiting = 0;
993 }
994 thread_sched_wakeup_next_thread(sched, th, false);
995}
996
997// running -> waiting
998//
999// This thread will run a dedicated task.
1000static void
1001thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately)
1002{
1003 thread_sched_lock(sched, th);
1004 {
1005 thread_sched_to_waiting_common(sched, th, yield_immediately);
1006 }
1007 thread_sched_unlock(sched, th);
1008}
1009
1010// mini utility func
1011// return true if any there are any interrupts
1012static bool
1013ubf_set(rb_thread_t *th, rb_unblock_function_t *func, void *arg, rb_atomic_t *event_serial)
1014{
1015 VM_ASSERT(func != NULL);
1016
1017 retry:
1018 if (RUBY_VM_INTERRUPTED(th->ec)) {
1019 RUBY_DEBUG_LOG("interrupted:0x%x", th->ec->interrupt_flag);
1020 return true;
1021 }
1022
1023 rb_native_mutex_lock(&th->interrupt_lock);
1024 {
1025 if (!th->ec->raised_flag && RUBY_VM_INTERRUPTED(th->ec)) {
1026 rb_native_mutex_unlock(&th->interrupt_lock);
1027 goto retry;
1028 }
1029
1030 VM_ASSERT(th->unblock.func == NULL);
1031 th->unblock.func = func;
1032 th->unblock.arg = arg;
1033 if (event_serial) {
1034 rb_atomic_t prev_serial = RUBY_ATOMIC_FETCH_ADD(th->unblock.event_serial, 1);
1035 *event_serial = prev_serial+1;
1036 }
1037 }
1038 rb_native_mutex_unlock(&th->interrupt_lock);
1039
1040 return false;
1041}
1042
1043static void
1044ubf_clear(rb_thread_t *th, bool clear_serial)
1045{
1046 rb_native_mutex_lock(&th->interrupt_lock);
1047 {
1048 th->unblock.func = NULL;
1049 th->unblock.arg = NULL;
1050 if (clear_serial) {
1051 RUBY_ATOMIC_ADD(th->unblock.event_serial, 1);
1052 }
1053 }
1054 rb_native_mutex_unlock(&th->interrupt_lock);
1055}
1056
1057static void
1058ubf_waiting(void *ptr)
1059{
1060 rb_thread_t *th = (rb_thread_t *)ptr;
1061 struct rb_thread_sched *sched = TH_SCHED(th);
1062
1063 // only once. it is safe because th->interrupt_lock is already acquired.
1064 th->unblock.func = NULL;
1065 th->unblock.arg = NULL;
1066
1067 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
1068
1069 thread_sched_lock(sched, th);
1070 {
1071 if (sched->running == th || th->sched.node.is_ready) {
1072 // not sleeping yet, or a deadline already put it back in line
1073 }
1074 else {
1075 thread_sched_to_ready_common(sched, th, true, false);
1076
1077 // If the turn is taken, th stays parked until the running thread yields.
1078 // For a timed wait, wake it early anyway: it re-parks at once, but its
1079 // wakeup then runs on another core in parallel with the running thread,
1080 // off the handoff path. An untimed wait has no post-wake bookkeeping
1081 // worth pipelining, so it skips the extra futex round.
1082 if (sched->running != th && th->sched.waiting_timed &&
1083 th->nt != NULL && th_has_dedicated_nt(th)) {
1084 rb_native_cond_signal(&th->nt->readyq);
1085 }
1086 }
1087 }
1088 thread_sched_unlock(sched, th);
1089}
1090
1091// running -> waiting
1092//
1093// This thread will sleep until other thread wakeup the thread. `end` is an
1094// absolute deadline, NULL to sleep until woken; only a dedicated native thread,
1095// which parks on its own condvar, can take one.
1096static void
1097thread_sched_to_waiting_until_wakeup(struct rb_thread_sched *sched, rb_thread_t *th, const rb_hrtime_t *end)
1098{
1099 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
1100
1101 VM_ASSERT(end == NULL || th_has_dedicated_nt(th));
1102
1103 RB_VM_SAVE_MACHINE_CONTEXT(th);
1104
1105
1106 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
1107
1108 thread_sched_lock(sched, th);
1109 {
1110 // NOTE: there's a lock ordering inversion here with the ubf call, but it's benign.
1111 if (ubf_set(th, ubf_waiting, (void *)th, NULL)) {
1112 RUBY_DEBUG_LOG("th:%u interrupted", rb_th_serial(th));
1113 }
1114 else {
1115 bool can_direct_transfer = !th_has_dedicated_nt(th);
1116 th->sched.waiting_timed = (end != NULL); // never true here for M:N (end is NULL)
1117 // NOTE: th->status is set before and after this sleep outside of this function in `sleep_forever`
1118 thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
1119 thread_sched_wait_running_turn(sched, th, can_direct_transfer, end);
1120 th->sched.waiting_timed = false;
1121 }
1122 }
1123 thread_sched_unlock(sched, th);
1124
1125 ubf_clear(th, false);
1126}
1127
1128// run another thread in the ready queue.
1129// continue to run if there are no ready threads.
1130static void
1131thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th)
1132{
1133 RUBY_DEBUG_LOG("th:%d sched->readyq_cnt:%d", (int)th->serial, sched->readyq_cnt);
1134
1135 thread_sched_lock(sched, th);
1136 {
1137 if (!ccan_list_empty(&sched->readyq)) {
1138 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
1139 thread_sched_wakeup_next_thread(sched, th, !th_has_dedicated_nt(th));
1140 bool can_direct_transfer = !th_has_dedicated_nt(th);
1141 thread_sched_to_ready_common(sched, th, false, can_direct_transfer);
1142 thread_sched_wait_running_turn(sched, th, can_direct_transfer, NULL);
1143 th->status = THREAD_RUNNABLE;
1144 }
1145 else {
1146 VM_ASSERT(sched->readyq_cnt == 0);
1147 }
1148 }
1149 thread_sched_unlock(sched, th);
1150}
1151
1152void
1153rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
1154{
1155 rb_native_mutex_initialize(&sched->lock_);
1156
1157#if VM_CHECK_MODE
1158 sched->lock_owner = NULL;
1159#endif
1160
1161 ccan_list_head_init(&sched->readyq);
1162 sched->readyq_cnt = 0;
1163 ccan_list_node_init(&sched->grq_node); // self-linked = not enqueued
1164 ccan_list_node_init(&sched->timeslice_node);
1165
1166#if USE_MN_THREADS
1167 // A Ractor's threads are M:N unless RUBY_MN_THREADS turns it off entirely;
1168 // the main Ractor's setting is decided in ruby_mn_threads_params().
1169 if (!atfork) sched->enable_mn_threads = mn_threads_enabled_p();
1170#endif
1171}
1172
1173static void
1174coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_context *transfer_to, bool to_dead)
1175{
1176#ifdef RUBY_ASAN_ENABLED
1177 void **fake_stack = to_dead ? NULL : &transfer_from->fake_stack;
1178 __sanitizer_start_switch_fiber(fake_stack, transfer_to->stack_base, transfer_to->stack_size);
1179#endif
1180
1181#if defined(COROUTINE_SANITIZE_THREAD)
1182 /* Tell TSan we are switching to transfer_to's fiber before the stack
1183 * switch, so its per-thread shadow stack stays bound to the right
1184 * coroutine. */
1185 __tsan_switch_to_fiber(transfer_to->tsan_fiber, 0);
1186#endif
1187
1189 struct coroutine_context *returning_from = coroutine_transfer(transfer_from, transfer_to);
1190
1191 /* if to_dead was passed, the caller is promising that this coroutine is finished and it should
1192 * never be resumed! */
1193 VM_ASSERT(!to_dead);
1194#ifdef RUBY_ASAN_ENABLED
1195 __sanitizer_finish_switch_fiber(transfer_from->fake_stack,
1196 (const void**)&returning_from->stack_base, &returning_from->stack_size);
1197#endif
1198}
1199
1200static void
1201thread_sched_switch0(struct coroutine_context *current_cont, rb_thread_t *next_th, struct rb_native_thread *nt, bool to_dead)
1202{
1203 VM_ASSERT(!nt->dedicated);
1204 VM_ASSERT(next_th->nt == NULL);
1205
1206 RUBY_DEBUG_LOG("next_th:%u", rb_th_serial(next_th));
1207
1208 // this direct transfer serves next_th without a dequeue; cancel its
1209 // Ractor's outstanding grq entry (no-op when nothing is enqueued)
1210 ractor_sched_cancel_enq(next_th->vm, TH_SCHED(next_th));
1211
1212 ruby_thread_set_native(next_th);
1213 native_thread_assign(nt, next_th);
1214
1215 coroutine_transfer0(current_cont, next_th->sched.context, to_dead);
1216}
1217
1218static void
1219thread_sched_switch(rb_thread_t *cth, rb_thread_t *next_th)
1220{
1221 struct rb_native_thread *nt = cth->nt;
1222 native_thread_assign(NULL, cth);
1223 RUBY_DEBUG_LOG("th:%u->%u on nt:%d", rb_th_serial(cth), rb_th_serial(next_th), nt->serial);
1224 // never final: a thread ends only through co_start's epilogue transfer. The
1225 // main thread is THREAD_KILLED (rb_ec_cleanup) while it still parks here.
1226 thread_sched_switch0(cth->sched.context, next_th, nt, false);
1227}
1228
1229#if VM_CHECK_MODE > 0
1231static unsigned int
1232grq_size(rb_vm_t *vm, rb_ractor_t *cr)
1233{
1234 ASSERT_ractor_sched_locked(vm, cr);
1235
1236 rb_ractor_t *r, *prev_r = NULL;
1237 unsigned int i = 0;
1238
1239 ccan_list_for_each(&vm->ractor.sched.grq, r, threads.sched.grq_node) {
1240 i++;
1241
1242 VM_ASSERT(r != prev_r);
1243 prev_r = r;
1244 }
1245 return i;
1246}
1247#endif
1248
1249// A native thread enters/leaves an epilogue that outlives its Ractor: from
1250// the increment until the decrement, ruby_vm_destruct waits for it below.
1251// The increment must happen while the VM still counts the thread's Ractor,
1252// so that the two never look absent at the same time.
1253void
1254rb_thread_sched_winding_begin(rb_vm_t *vm)
1255{
1256 RUBY_ATOMIC_INC(vm->ractor.sched.winding_cnt);
1257}
1258
1259void
1260rb_thread_sched_winding_end(rb_vm_t *vm)
1261{
1262 VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.winding_cnt) > 0);
1263 RUBY_ATOMIC_DEC(vm->ractor.sched.winding_cnt);
1264}
1265
1266// ruby_vm_destruct: wait until no native thread is between a coroutine
1267// epilogue and its reclaim -- past that point the reclaim frees through the
1268// (about to be destroyed) objspace and reads the (about to be unset) VM.
1269// Runs without the VM lock, which the epilogue needs to progress.
1270void
1271rb_thread_sched_wait_winding(rb_vm_t *vm)
1272{
1273 while (RUBY_ATOMIC_LOAD(vm->ractor.sched.winding_cnt) > 0) {
1274 native_thread_yield();
1275 }
1276}
1277
1278// A direct service of a runnable thread (direct transfer or the hot-thread
1279// steal) bypasses the grq; cancel the Ractor's outstanding entry so that
1280// "enqueued <=> runnable and unserved" keeps holding. The caller holds the
1281// per-Ractor sched lock, so no concurrent enqueue can relink the node: a
1282// self-linked read needs no lock (the common case -- direct switches whose
1283// transition never enqueued). A linked read can race only with a dequeue,
1284// hence the recheck under the grq lock.
1285static void
1286ractor_sched_cancel_enq(rb_vm_t *vm, struct rb_thread_sched *sched)
1287{
1288 if (sched->grq_node.next != &sched->grq_node) {
1289 ractor_sched_lock(vm, NULL);
1290 {
1291 if (sched->grq_node.next != &sched->grq_node) {
1292 ccan_list_del_init(&sched->grq_node);
1293 VM_ASSERT(vm->ractor.sched.grq_cnt > 0);
1294 vm->ractor.sched.grq_cnt--;
1295 }
1296 }
1297 ractor_sched_unlock(vm, NULL);
1298 }
1299}
1300
1301static void
1302ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r)
1303{
1304 struct rb_thread_sched *sched = &r->threads.sched;
1305 rb_ractor_t *cr = NULL; // timer thread can call this function
1306
1307 VM_ASSERT(sched->running != NULL);
1308 VM_ASSERT(sched->running->nt == NULL);
1309
1310 ractor_sched_lock(vm, cr);
1311 {
1312 // Precondition: not already enqueued (the grq_node is self-linked).
1313 // This holds because every service of a runnable-but-unserved thread
1314 // either dequeues the entry (the nt scheduling loop) or cancels it
1315 // (direct transfers / the hot-thread steal; see
1316 // ractor_sched_cancel_enq) -- re-adding a linked node would corrupt
1317 // the queue, so check unconditionally (a CHECK-mode-only assert
1318 // would miss it: the race needs timing that CHECK builds perturb).
1319 if (sched->grq_node.next != &sched->grq_node) {
1320 rb_bug("ractor_sched_enq: already enqueued");
1321 }
1322 ccan_list_add_tail(&vm->ractor.sched.grq, &sched->grq_node);
1323 vm->ractor.sched.grq_cnt++;
1324 VM_ASSERT(grq_size(vm, cr) == vm->ractor.sched.grq_cnt);
1325
1326 RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u th:%u grq_cnt:%u", rb_ractor_id(r), rb_th_serial(sched->running), vm->ractor.sched.grq_cnt);
1327
1328 rb_native_cond_signal(&vm->ractor.sched.cond);
1329
1330 // The signal reaches a parked snt, and a running one revisits the
1331 // queue in ractor_sched_deq before it can wait (same lock as here).
1332 // With every snt dedicated or retired, only the timer thread's
1333 // timeout branch can serve the entry or widen the pool: wake it
1334 // (a no-op unless it sleeps untimed).
1335 if (RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0) {
1336 timer_thread_wakeup_locked(vm);
1337 }
1338
1339 // ractor_sched_dump(vm);
1340 }
1341 ractor_sched_unlock(vm, cr);
1342}
1343
1344
1345#ifndef MINIMUM_SNT
1346// make at least MINIMUM_SNT snts for debug.
1347#define MINIMUM_SNT 0
1348#endif
1349
1350/* A shared thread woken with nothing to run is one whose turn another thread
1351 * took first. After this many in a row it gives itself back: the queue keeps
1352 * running dry, so the pool is wider than the work. 0 retires on the first one
1353 * and is too eager to be useful; a negative value keeps every thread. */
1354#ifndef SNT_IDLE_RETIRE
1355#define SNT_IDLE_RETIRE 3
1356#endif
1357
1358/* Never give the last shared thread back. With none left an enqueue has nobody
1359 * to signal, and the only code that makes one runs on the timer thread's
1360 * timeout branch, which is reached only once it has seen a backlog. */
1361#define SNT_KEEP_MINIMUM (MINIMUM_SNT > 1 ? MINIMUM_SNT : 1)
1362
1363static rb_ractor_t *
1364ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr, bool can_retire)
1365{
1366 rb_ractor_t *r;
1367 int idle_streak = 0; // consecutive pops that found the queue empty
1368
1369 ractor_sched_lock(vm, cr);
1370 {
1371 RUBY_DEBUG_LOG("empty? %d", ccan_list_empty(&vm->ractor.sched.grq));
1372 // ractor_sched_dump(vm);
1373
1374 VM_ASSERT(rb_current_execution_context(false) == NULL);
1375 VM_ASSERT(grq_size(vm, cr) == vm->ractor.sched.grq_cnt);
1376
1377 while ((r = ccan_list_pop(&vm->ractor.sched.grq, rb_ractor_t, threads.sched.grq_node)) == NULL) {
1378 RUBY_DEBUG_LOG("wait grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
1379
1380 if (can_retire && SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
1381 (int)RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) > SNT_KEEP_MINIMUM) {
1382 RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
1383 RUBY_DEBUG_LOG("retire, snt_cnt:%d", (int)vm->ractor.sched.snt_cnt);
1384 break; // returning NULL ends this nt; see the caller
1385 }
1386
1387 ractor_sched_set_unlocked(vm, cr);
1388 rb_native_cond_wait(&vm->ractor.sched.cond, &vm->ractor.sched.lock);
1389 ractor_sched_set_locked(vm, cr);
1390
1391 RUBY_DEBUG_LOG("wakeup grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
1392 }
1393
1394 VM_ASSERT(rb_current_execution_context(false) == NULL);
1395
1396 if (r) {
1397 ccan_list_node_init(&r->threads.sched.grq_node); // back to self-linked
1398 VM_ASSERT(vm->ractor.sched.grq_cnt > 0);
1399 vm->ractor.sched.grq_cnt--;
1400 RUBY_DEBUG_LOG("r:%d grq_cnt:%u", (int)rb_ractor_id(r), vm->ractor.sched.grq_cnt);
1401 }
1402 else {
1403 // the retire branch is the only way out of the loop without a ractor
1404 VM_ASSERT(idle_streak > SNT_IDLE_RETIRE);
1405 }
1406 }
1407 ractor_sched_unlock(vm, cr);
1408
1409 return r;
1410}
1411
1412void rb_ractor_lock_self(rb_ractor_t *r);
1413void rb_ractor_unlock_self(rb_ractor_t *r);
1414
1415// The current thread for a ractor is put to "sleep" (descheduled in the STOPPED_FOREVER state) waiting for
1416// a ractor action to wake it up.
1417void
1418rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ubf_arg)
1419{
1420 // ractor lock of cr is acquired
1421
1422 RUBY_DEBUG_LOG("start%s", "");
1423
1424 rb_thread_t * volatile th = rb_ec_thread_ptr(ec);
1425 struct rb_thread_sched *sched = TH_SCHED(th);
1426 struct ractor_waiter *waiter = (struct ractor_waiter*)ubf_arg;
1427
1428 if (ubf_set(th, ubf, ubf_arg, &waiter->event_serial)) {
1429 // interrupted
1430 return;
1431 }
1432
1433 thread_sched_lock(sched, th);
1434 rb_ractor_unlock_self(cr);
1435 {
1436 // A dedicated native thread takes the deadline on the very condvar a wakeup
1437 // signals. An M:N thread has no condvar of its own, so its deadline goes to
1438 // the timer thread, which then wakes it the way rb_ractor_sched_wakeup() does.
1439 bool dedicated = th_has_dedicated_nt(th);
1440 const rb_hrtime_t *end_p = NULL;
1441 bool armed = false, expired = false;
1442
1443 if (waiter->end) {
1444 if (dedicated) {
1445 end_p = waiter->end;
1446 }
1447 else {
1448 // the timer wheel takes a relative timeout
1449 rb_hrtime_t now = rb_hrtime_now();
1450 rb_hrtime_t rel = *waiter->end > now ? *waiter->end - now : 0;
1451
1452 armed = ractor_sched_timeout_arm(th, &rel);
1453 expired = !armed;
1454 }
1455 }
1456
1457 if (expired) {
1458 RUBY_DEBUG_LOG("expired before sleep%s", "");
1459 }
1460 else if (armed && th->sched.waiting_reason.flags == thread_sched_waiting_none) {
1461 // the timer thread already took this thread out of the wheel; bump the
1462 // serial so that it does not try to wake a thread that never slept
1463 th->sched.event_serial++;
1464 }
1465 else {
1466 // setup sleep
1467 bool can_direct_transfer = !dedicated;
1468 RB_VM_SAVE_MACHINE_CONTEXT(th);
1469 th->status = THREAD_STOPPED_FOREVER;
1470 th->sched.waiting_timed = (end_p != NULL); // never true here for M:N (end_p is NULL)
1471 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
1472 thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
1473 // sleep
1474 thread_sched_wait_running_turn(sched, th, can_direct_transfer, end_p);
1475 th->sched.waiting_timed = false;
1476 th->status = THREAD_RUNNABLE;
1477
1478 // whoever woke this thread took the timeout back first
1479 VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
1480 }
1481 }
1482 thread_sched_unlock(sched, th);
1483 rb_ractor_lock_self(cr);
1484
1485 ubf_clear(th, true);
1486
1487 RUBY_DEBUG_LOG("end%s", "");
1488}
1489
1490void
1491rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *r_th)
1492{
1493 // ractor lock of r acquired
1494 struct rb_thread_sched *sched = TH_SCHED(r_th);
1495
1496 RUBY_DEBUG_LOG("r:%u th:%d", (unsigned int)rb_ractor_id(r), r_th->serial);
1497
1498 thread_sched_lock(sched, r_th);
1499 {
1500 if (r_th->status == THREAD_STOPPED_FOREVER) {
1501 RUBY_ATOMIC_ADD(r_th->unblock.event_serial, 1);
1502
1503 // r_th must not resume with a wheel entry left behind: take its timeout
1504 // back, as ubf_event_waiting() does. Only r_th arms it, and it is
1505 // parked here, so reading the flags without the timer lock is safe.
1506 if (r_th->sched.waiting_reason.flags != thread_sched_waiting_none) {
1507 ractor_sched_timeout_disarm(r_th);
1508 }
1509
1510 // a timeout that fired first may have made r_th runnable already: waking
1511 // it twice would put it on the readyq twice
1512 if (sched->running != r_th && !r_th->sched.node.is_ready) {
1513 r_th->sched.event_serial++; // a timeout still armed must not wake it again
1514 thread_sched_to_ready_common(sched, r_th, true, false);
1515 }
1516 }
1517 }
1518 thread_sched_unlock(sched, r_th);
1519}
1520
1521static bool
1522ractor_sched_barrier_completed_p(rb_vm_t *vm)
1523{
1524 // The snapshot barrier_running_cnt is taken by the barrier's walk and
1525 // decremented by counted deregistrations; no rescan is needed here.
1526 RUBY_DEBUG_LOG("run:%u wait:%u", vm->ractor.sched.barrier_running_cnt, vm->ractor.sched.barrier_joined_cnt);
1527 VM_ASSERT(vm->ractor.sched.barrier_running_cnt - 1 >= vm->ractor.sched.barrier_joined_cnt);
1528
1529 return (vm->ractor.sched.barrier_running_cnt - vm->ractor.sched.barrier_joined_cnt) == 1;
1530}
1531
1532void
1533rb_ractor_sched_barrier_start(rb_vm_t *vm, rb_ractor_t *cr)
1534{
1535 VM_ASSERT(cr == GET_RACTOR());
1536 VM_ASSERT(vm->ractor.sync.lock_owner == cr); // VM is locked
1537 VM_ASSERT(!vm->ractor.sched.barrier_is_waiting);
1538 VM_ASSERT(vm->ractor.sched.barrier_joined_cnt == 0);
1539 VM_ASSERT(vm->ractor.sched.barrier_ractor == NULL);
1540 VM_ASSERT(vm->ractor.sched.barrier_lock_rec == 0);
1541
1542 RUBY_DEBUG_LOG("start serial:%u", vm->ractor.sched.barrier_serial);
1543
1544 unsigned int lock_rec;
1545
1546 ractor_sched_lock(vm, cr);
1547 {
1548 RUBY_ATOMIC_SET(vm->ractor.sched.barrier_is_waiting, 1);
1549 vm->ractor.sched.barrier_ractor = cr;
1550 vm->ractor.sched.barrier_lock_rec = vm->ractor.sync.lock_rec;
1551
1552 // release VM lock
1553 lock_rec = vm->ractor.sync.lock_rec;
1554 vm->ractor.sync.lock_rec = 0;
1555 vm->ractor.sync.lock_owner = NULL;
1556 rb_native_mutex_unlock(&vm->ractor.sync.lock);
1557
1558 // Interrupt all running threads: running_dnts plus each snt's running_th.
1559 // A switch before this scan is visible to it; one after it sees
1560 // barrier_is_waiting (set above) and waits.
1561 // Interrupt and count every registered runner, stamping each nt so a
1562 // deregistration during this barrier knows it was counted.
1563 rb_thread_t *ith;
1564 unsigned int running_cnt = 0;
1565 uint32_t serial = vm->ractor.sched.barrier_serial;
1566
1567 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
1568 {
1569 struct rb_native_thread *dnt;
1570 ccan_list_for_each(&vm->ractor.sched.ntlist.running_dnts, dnt, running_dnts_node) {
1571 ith = dnt->running_thread;
1572 dnt->barrier_counted_serial = serial;
1573 running_cnt++;
1574 if (ith->ractor != cr) {
1575 RUBY_DEBUG_LOG("barrier request to th:%u", rb_th_serial(ith));
1576 RUBY_VM_SET_VM_BARRIER_INTERRUPT(ith->ec);
1577 }
1578 }
1579
1580 struct rb_native_thread *nt;
1581 ccan_list_for_each(&vm->ractor.sched.ntlist.snts, nt, snts_node) {
1582 rb_native_mutex_lock(&nt->running_th_lock);
1583 {
1584 ith = nt->running_th;
1585 if (ith != NULL) {
1586 nt->barrier_counted_serial = serial;
1587 running_cnt++;
1588 if (ith->ractor != cr) {
1589 RUBY_DEBUG_LOG("barrier request to th:%u", rb_th_serial(ith));
1590 RUBY_VM_SET_VM_BARRIER_INTERRUPT(ith->ec);
1591 }
1592 }
1593 }
1594 rb_native_mutex_unlock(&nt->running_th_lock);
1595 }
1596 }
1597 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
1598
1599 vm->ractor.sched.barrier_running_cnt = running_cnt;
1600
1601 // wait for other ractors
1602 while (!ractor_sched_barrier_completed_p(vm)) {
1603 ractor_sched_set_unlocked(vm, cr);
1604 rb_native_cond_wait(&vm->ractor.sched.barrier_complete_cond, &vm->ractor.sched.lock);
1605 ractor_sched_set_locked(vm, cr);
1606 }
1607
1608 RUBY_DEBUG_LOG("completed seirial:%u", vm->ractor.sched.barrier_serial);
1609
1610 // no other ractors are there
1611 vm->ractor.sched.barrier_serial++;
1612 vm->ractor.sched.barrier_joined_cnt = 0;
1613 rb_native_cond_broadcast(&vm->ractor.sched.barrier_release_cond);
1614
1615 // acquire VM lock
1616 rb_native_mutex_lock(&vm->ractor.sync.lock);
1617 vm->ractor.sync.lock_rec = lock_rec;
1618 vm->ractor.sync.lock_owner = cr;
1619 }
1620
1621 // do not release ractor_sched_lock and there is no newly added (resumed) thread
1622 // thread_sched_setup_running_threads
1623}
1624
1625// called from vm_lock_leave if the vm_lock used for barrierred
1626void
1627rb_ractor_sched_barrier_end(rb_vm_t *vm, rb_ractor_t *cr)
1628{
1629 RUBY_DEBUG_LOG("serial:%u", (unsigned int)vm->ractor.sched.barrier_serial - 1);
1630 VM_ASSERT(vm->ractor.sched.barrier_is_waiting);
1631 VM_ASSERT(vm->ractor.sched.barrier_ractor);
1632 VM_ASSERT(vm->ractor.sched.barrier_lock_rec > 0);
1633
1634 RUBY_ATOMIC_SET(vm->ractor.sched.barrier_is_waiting, 0);
1635 vm->ractor.sched.barrier_ractor = NULL;
1636 vm->ractor.sched.barrier_lock_rec = 0;
1637 ractor_sched_unlock(vm, cr);
1638}
1639
1640static void
1641ractor_sched_barrier_join_signal_locked(rb_vm_t *vm)
1642{
1643 if (ractor_sched_barrier_completed_p(vm)) {
1644 rb_native_cond_signal(&vm->ractor.sched.barrier_complete_cond);
1645 }
1646}
1647
1648static void
1649ractor_sched_barrier_join_wait_locked(rb_vm_t *vm, rb_thread_t *th)
1650{
1651 VM_ASSERT(vm->ractor.sched.barrier_is_waiting);
1652
1653 unsigned int barrier_serial = vm->ractor.sched.barrier_serial;
1654
1655 while (vm->ractor.sched.barrier_serial == barrier_serial) {
1656 RUBY_DEBUG_LOG("sleep serial:%u", barrier_serial);
1657 RB_VM_SAVE_MACHINE_CONTEXT(th);
1658
1659 rb_ractor_t *cr = th->ractor;
1660 ractor_sched_set_unlocked(vm, cr);
1661 rb_native_cond_wait(&vm->ractor.sched.barrier_release_cond, &vm->ractor.sched.lock);
1662 ractor_sched_set_locked(vm, cr);
1663
1664 RUBY_DEBUG_LOG("wakeup serial:%u", barrier_serial);
1665 }
1666}
1667
1668void
1669rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr)
1670{
1671 VM_ASSERT(cr->threads.sched.running != NULL); // running ractor
1672 VM_ASSERT(cr == GET_RACTOR());
1673 VM_ASSERT(vm->ractor.sync.lock_owner == NULL); // VM is locked, but owner == NULL
1674 VM_ASSERT(vm->ractor.sched.barrier_is_waiting); // VM needs barrier sync
1675
1676#if USE_RUBY_DEBUG_LOG || VM_CHECK_MODE > 0
1677 unsigned int barrier_serial = vm->ractor.sched.barrier_serial;
1678#endif
1679
1680 RUBY_DEBUG_LOG("join");
1681
1682 rb_native_mutex_unlock(&vm->ractor.sync.lock);
1683 {
1684 VM_ASSERT(vm->ractor.sched.barrier_is_waiting); // VM needs barrier sync
1685 VM_ASSERT(vm->ractor.sched.barrier_serial == barrier_serial);
1686
1687 ractor_sched_lock(vm, cr);
1688 {
1689 // running_cnt
1690 /* Every joiner is a member of the running set: a dying thread
1691 * leaves the living set before handing over its scheduler slot. */
1692 vm->ractor.sched.barrier_joined_cnt++;
1693 RUBY_DEBUG_LOG("waiting_cnt:%u serial:%u", vm->ractor.sched.barrier_joined_cnt, barrier_serial);
1694
1695 ractor_sched_barrier_join_signal_locked(vm);
1696 ractor_sched_barrier_join_wait_locked(vm, cr->threads.sched.running);
1697 }
1698 ractor_sched_unlock(vm, cr);
1699 }
1700
1701 rb_native_mutex_lock(&vm->ractor.sync.lock);
1702 // VM locked here
1703}
1704
1705// Called when the ractor holding this sched is freed. A drained sched can
1706// still be on timeslice.scheds (pruning is lazy); an unlisted node is
1707// self-linked (fork re-inits them all), making this del a no-op.
1708void
1709rb_thread_sched_destroy(struct rb_thread_sched *sched)
1710{
1711 rb_vm_t *vm = GET_VM();
1712
1713 rb_native_mutex_lock(&vm->ractor.sched.timeslice.lock);
1714 {
1715 ccan_list_del_init(&sched->timeslice_node);
1716 }
1717 rb_native_mutex_unlock(&vm->ractor.sched.timeslice.lock);
1718}
1719
1720#if defined(HAVE_WORKING_FORK)
1721static void rb_internal_thread_event_hooks_rw_lock_atfork(void);
1722
1723static void
1724thread_sched_atfork(struct rb_thread_sched *sched)
1725{
1726 current_fork_gen++;
1727 rb_thread_sched_init(sched, true);
1728 rb_thread_t *th = GET_THREAD();
1729 rb_vm_t *vm = GET_VM();
1730
1731 if (th_has_dedicated_nt(th)) {
1732 vm->ractor.sched.snt_cnt = 0;
1733#if USE_RUBY_DEBUG_LOG
1734 vm->ractor.sched.dnt_cnt = 1;
1735#endif
1736 }
1737 else {
1738 vm->ractor.sched.snt_cnt = 1;
1739#if USE_RUBY_DEBUG_LOG
1740 vm->ractor.sched.dnt_cnt = 0;
1741#endif
1742 }
1743
1744 rb_native_mutex_initialize(&vm->ractor.sched.lock);
1745#if VM_CHECK_MODE > 0
1746 vm->ractor.sched.lock_owner = NULL;
1747 vm->ractor.sched.locked = false;
1748#endif
1749
1750 // rb_native_cond_destroy(&vm->ractor.sched.cond);
1751 rb_native_cond_initialize(&vm->ractor.sched.cond);
1752 rb_native_cond_initialize(&vm->ractor.sched.barrier_complete_cond);
1753 rb_native_cond_initialize(&vm->ractor.sched.barrier_release_cond);
1754
1755 ccan_list_head_init(&vm->ractor.sched.grq);
1756 vm->ractor.sched.grq_cnt = 0; // the list was just emptied; reset the count with it
1757 // A fork during a VM barrier leaves the child with barrier state that can
1758 // never complete (the other ractors are gone); reset it like the rest.
1759 vm->ractor.sched.barrier_is_waiting = 0; // single-threaded child
1760 vm->ractor.sched.barrier_joined_cnt = 0;
1761 vm->ractor.sched.barrier_ractor = NULL;
1762 vm->ractor.sched.barrier_lock_rec = 0;
1763 // Threads that were winding down in the parent do not exist in the child;
1764 // without this reset the child's ruby_vm_destruct would wait for their
1765 // reclaim (which never comes) forever.
1766 vm->ractor.sched.winding_cnt = 0;
1767 rb_native_mutex_initialize(&vm->ractor.sched.ntlist.lock);
1768 ccan_list_head_init(&vm->ractor.sched.ntlist.running_dnts);
1769 ccan_list_head_init(&vm->ractor.sched.ntlist.snts); // those nts are gone
1770 rb_native_mutex_initialize(&vm->ractor.sched.timeslice.lock);
1771 ccan_list_head_init(&vm->ractor.sched.timeslice.scheds);
1772 rb_native_mutex_initialize(&th->nt->running_th_lock); // a scan could hold it at fork
1773 th->nt->running_th = NULL; // th re-records itself below
1774 th->nt->retiring = false; // the pool it had no room in is gone
1775 native_main_thread_atfork(); // this pthread is the process's main one now
1776 // Fork can copy nodes linked (or torn mid-link); re-init every sched's
1777 // node so rb_thread_sched_destroy's del_init stays a no-op for them.
1778 rb_ractor_t *r;
1779 ccan_list_for_each(&vm->ractor.set, r, vmlr_node) {
1780 ccan_list_node_init(&r->threads.sched.timeslice_node);
1781 }
1782 ccan_list_for_each(&vm->ractor.terminated_set, r, vmlr_node) {
1783 ccan_list_node_init(&r->threads.sched.timeslice_node);
1784 }
1785 // th re-records itself below; the parent's record did not survive the lists
1786 if (th->nt && th->nt->dedicated == 0) {
1787 // surviving on an snt: put that nt back on the (just emptied) snts
1788 // list, or the scans could not see this thread's record
1789 ccan_list_add(&vm->ractor.sched.ntlist.snts, &th->nt->snts_node);
1790 }
1791
1792#if USE_MN_THREADS
1793 nt_machine_stack_atfork();
1794#endif
1795 rb_internal_thread_event_hooks_rw_lock_atfork();
1796
1797 VM_ASSERT(sched->is_running);
1798
1799 if (sched->running != th) {
1800 thread_sched_to_running(sched, th);
1801 }
1802 else {
1803 thread_sched_setup_running_threads(sched, th->ractor, vm, th, NULL);
1804 }
1805
1806#ifdef RB_THREAD_T_HAS_NATIVE_ID
1807 if (th->nt) {
1808 th->nt->tid = get_native_thread_id();
1809 }
1810#endif
1811}
1812
1813#endif
1814
1815extern int ruby_mn_threads_enabled;
1816
1817void
1818ruby_mn_threads_params(void)
1819{
1820 rb_vm_t *vm = GET_VM();
1821 rb_ractor_t *main_ractor = GET_RACTOR();
1822
1823 // RUBY_MN_THREADS: -1 = nothing is M:N, 0 = the default, 1 = the main
1824 // Ractor's threads too, 2 = the main thread as well (see
1825 // thread_sched_main_to_shared). The main Ractor's sched already exists
1826 // here, so it is set rather than defaulted.
1827 const char *mn_threads_cstr = getenv("RUBY_MN_THREADS");
1828 int mn_threads = (USE_MN_THREADS && mn_threads_cstr) ? atoi(mn_threads_cstr) : 0;
1829
1830 mn_threads_mode = mn_threads;
1831 if (mn_threads > 0) {
1832 ruby_mn_threads_enabled = mn_threads;
1833 }
1834 // =2 publishes it from thread_sched_main_to_shared, with the main nt's
1835 // pool entry, so that the timer thread cannot mint an snt in between
1836 main_ractor->threads.sched.enable_mn_threads = mn_threads == 1;
1837
1838 const char *max_cpu_cstr = getenv("RUBY_MAX_CPU");
1839 int max_cpu = native_thread_default_max_cpu();
1840
1841 if (USE_MN_THREADS && max_cpu_cstr) {
1842 int given_max_cpu = atoi(max_cpu_cstr);
1843 if (given_max_cpu > 0) {
1844 max_cpu = given_max_cpu;
1845 }
1846 }
1847
1848 vm->ractor.sched.max_cpu = max_cpu;
1849
1850#if USE_MN_THREADS
1851 if (mn_threads >= 2) {
1852 thread_sched_main_to_shared(GET_THREAD());
1853 }
1854#endif
1855}
1856
1857static void
1858native_thread_dedicated_inc(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_thread *nt)
1859{
1860 RUBY_DEBUG_LOG("nt:%d %d->%d", nt->serial, nt->dedicated, nt->dedicated + 1);
1861
1862 if (nt->dedicated == 0) {
1863 // Lock-free; pairs with ractor_sched_enq (enq: grq_cnt up then read
1864 // snt_cnt / here: snt_cnt down then read grq_cnt) against lost wakeups.
1865 if (RUBY_ATOMIC_FETCH_SUB(vm->ractor.sched.snt_cnt, 1) == 1) {
1866 // the last snt went dedicated; pending entries need the timer thread
1867 ractor_sched_lock(vm, cr);
1868 {
1869 if (vm->ractor.sched.grq_cnt > 0) {
1870 timer_thread_wakeup_locked(vm);
1871 }
1872 }
1873 ractor_sched_unlock(vm, cr);
1874 }
1875#if USE_RUBY_DEBUG_LOG
1876 vm->ractor.sched.dnt_cnt++;
1877#endif
1878 }
1879
1880 nt->dedicated++;
1881}
1882
1883static void
1884native_thread_dedicated_dec(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_thread *nt)
1885{
1886 RUBY_DEBUG_LOG("nt:%d %d->%d", nt->serial, nt->dedicated, nt->dedicated - 1);
1887 VM_ASSERT(nt->dedicated > 0);
1888 nt->dedicated--;
1889
1890 if (nt->dedicated == 0) {
1891 // Rejoin under the max_cpu cap; with no room this nt retires and
1892 // belongs to neither count until it ends. The process's main nt
1893 // cannot end (its loop runs on a stack it would have to free), so
1894 // it always rejoins.
1895 const bool can_retire = native_thread_self_can_retire_p();
1896 while (1) {
1897 rb_atomic_t snt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
1898 if (!can_retire || snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
1899 if (RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt, snt + 1) == snt) break;
1900 }
1901 else {
1902 nt->retiring = true;
1903 break;
1904 }
1905 }
1906#if USE_RUBY_DEBUG_LOG
1907 vm->ractor.sched.dnt_cnt--;
1908#endif
1909 }
1910}
1911
1912static void
1913native_thread_assign(struct rb_native_thread *nt, rb_thread_t *th)
1914{
1915#if USE_RUBY_DEBUG_LOG
1916 if (nt) {
1917 if (th->nt) {
1918 RUBY_DEBUG_LOG("th:%d nt:%d->%d", (int)th->serial, (int)th->nt->serial, (int)nt->serial);
1919 }
1920 else {
1921 RUBY_DEBUG_LOG("th:%d nt:NULL->%d", (int)th->serial, (int)nt->serial);
1922 }
1923 }
1924 else {
1925 if (th->nt) {
1926 RUBY_DEBUG_LOG("th:%d nt:%d->NULL", (int)th->serial, (int)th->nt->serial);
1927 }
1928 else {
1929 RUBY_DEBUG_LOG("th:%d nt:NULL->NULL", (int)th->serial);
1930 }
1931 }
1932#endif
1933
1934 th->nt = nt;
1935}
1936
1937static int
1938native_thread_create_dedicated(rb_thread_t *th)
1939{
1940 th->nt = native_thread_alloc();
1941 th->nt->vm = th->vm;
1942 th->nt->running_thread = th;
1943 th->nt->dedicated = 1;
1944
1945 // vm stack
1946 size_t vm_stack_word_size = th->vm->default_params.thread_vm_stack_size / sizeof(VALUE);
1947 void *vm_stack = ruby_xmalloc(vm_stack_word_size * sizeof(VALUE));
1948 th->sched.malloc_stack = true;
1949 rb_ec_initialize_vm_stack(th->ec, vm_stack, vm_stack_word_size);
1950 th->sched.context_stack = vm_stack;
1951 th->sched.context_stack_size = vm_stack_word_size;
1952
1953 int err = native_thread_create0(th->nt);
1954 if (!err) {
1955 // setup
1956 thread_sched_to_ready(TH_SCHED(th), th);
1957 }
1958 return err;
1959}
1960
1961static void
1962call_thread_start_func_2(rb_thread_t *th)
1963{
1964 /* Capture the address of a local in this stack frame to mark the beginning of the
1965 machine stack for this thread. This is required even if we can tell the real
1966 stack beginning from the pthread API in native_thread_init_stack, because
1967 glibc stores some of its own data on the stack before calling into user code
1968 on a new thread, and replacing that data on fiber-switch would break it (see
1969 bug #13887) */
1970 VALUE stack_start = 0;
1971 VALUE *stack_start_addr = asan_get_real_stack_addr(&stack_start);
1972
1973 native_thread_init_stack(th, stack_start_addr);
1974 thread_start_func_2(th, th->ec->machine.stack_start);
1975}
1976
1977static void *
1978nt_start(void *ptr)
1979{
1980 struct rb_native_thread *nt = (struct rb_native_thread *)ptr;
1981 rb_vm_t *vm = nt->vm;
1982
1983 native_thread_setup_on_thread(nt);
1984
1985 // init tid
1986#ifdef RB_THREAD_T_HAS_NATIVE_ID
1987 nt->tid = get_native_thread_id();
1988#endif
1989
1990#if USE_RUBY_DEBUG_LOG && defined(RUBY_NT_SERIAL)
1991 ruby_nt_serial = nt->serial;
1992#endif
1993
1994 RUBY_DEBUG_LOG("nt:%u", nt->serial);
1995
1996 if (nt->dedicated) {
1997 // wait running turn
1998 rb_thread_t *th = nt->running_thread;
1999 struct rb_thread_sched *sched = TH_SCHED(th);
2000
2001 RUBY_DEBUG_LOG("on dedicated th:%u", rb_th_serial(th));
2002 ruby_thread_set_native(th);
2003
2004 thread_sched_lock(sched, th);
2005 {
2006 if (sched->running == th) {
2007 thread_sched_add_running_thread(sched, th);
2008 }
2009 thread_sched_wait_running_turn(sched, th, false, NULL);
2010 }
2011 thread_sched_unlock(sched, th);
2012
2013 // start threads
2014 call_thread_start_func_2(th);
2015 // TODO: allow to change to the SNT
2016 }
2017 else {
2018 coroutine_initialize_main(nt->nt_context);
2019 nt_snts_join(vm, nt);
2020
2021 bool retired = nt_shared_loop(nt);
2022 nt_snts_leave(vm, nt);
2023
2024 if (retired) {
2025 // The counts dropped this nt already; nothing can reference it now.
2026 RUBY_DEBUG_LOG("retired nt:%u", nt->serial);
2027 native_thread_destroy_self(nt);
2028 }
2029 }
2030
2031 return NULL;
2032}
2033
2034// join the snt list that the barrier/timeslice scans walk
2035static void
2036nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt)
2037{
2038 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
2039 {
2040 ccan_list_add(&vm->ractor.sched.ntlist.snts, &nt->snts_node);
2041 }
2042 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
2043}
2044
2045// Leaving the shared loop: every path back here deregistered first
2046// (park and death both precede the transfer), so only the snts entry
2047// is left to remove.
2048static void
2049nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt)
2050{
2051 VM_ASSERT(nt->running_th == NULL);
2052 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
2053 {
2054 ccan_list_del_init(&nt->snts_node);
2055 }
2056 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
2057}
2058
2059// The shared nt's scheduling loop: serve Ractors from the grq until this nt
2060// retires (returns true) or goes dedicated while running (returns false).
2061static bool
2062nt_shared_loop(struct rb_native_thread *nt)
2063{
2064 rb_vm_t *vm = nt->vm;
2065
2066 while (1) {
2067 RUBY_DEBUG_LOG("check next");
2068 if (nt->retiring) { // came back with no room in the shared pool
2069 return true;
2070 }
2071
2072 // asked every time: a fork leaves this loop's native thread as the
2073 // child's main one, which may not retire
2074 rb_ractor_t *r = ractor_sched_deq(vm, NULL, native_thread_self_can_retire_p());
2075
2076 if (r) {
2077 struct rb_thread_sched *sched = &r->threads.sched;
2078
2079 bool locked = true;
2080
2081 thread_sched_lock(sched, NULL);
2082 {
2083 rb_thread_t *next_th = sched->running;
2084
2085 if (next_th && next_th->nt == NULL) {
2086 RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
2087#if USE_MN_THREADS
2088 thread_sched_switch0(nt->nt_context, next_th, nt, false);
2089
2090 // If a coroutine terminated during the transfer, co_start
2091 // recorded it in nt->dead_co (switch0's return value is
2092 // backend-dependent, unusable; see thread_pthread.h).
2093 struct coroutine_context *dead_co = nt->dead_co;
2094 nt->dead_co = NULL;
2095 if (thread_sched_reclaim(dead_co)) {
2096 // it already released the sched lock before its
2097 // transfer (its Ractor may be gone): leave sched be.
2098 locked = false;
2099 }
2100#else
2101 thread_sched_switch0(nt->nt_context, next_th, nt, false);
2102#endif
2103 }
2104 else {
2105 RUBY_DEBUG_LOG("no schedulable threads -- next_th:%p", next_th);
2106 }
2107 }
2108 if (locked) {
2109 thread_sched_unlock(sched, NULL);
2110 }
2111 }
2112 else {
2113 // ractor_sched_deq retired this nt.
2114 return true;
2115 }
2116
2117 if (nt->dedicated) {
2118 // SNT becomes DNT while running
2119 return false;
2120 }
2121 }
2122}
2123
2124#if USE_MN_THREADS
2125// The scheduling loop of the process's main nt, entered when RUBY_MN_THREADS=2
2126// turned it shared: the process stack belongs to the main thread's context,
2127// so this loop runs on a coroutine of its own (thread_sched_main_to_shared).
2128static COROUTINE
2129nt_loop_co(struct coroutine_context *from, struct coroutine_context *self)
2130{
2131#ifdef RUBY_ASAN_ENABLED
2132 __sanitizer_finish_switch_fiber(self->fake_stack,
2133 (const void**)&from->stack_base, &from->stack_size);
2134#endif
2135 struct rb_native_thread *nt = (struct rb_native_thread *)self->argument;
2136
2137 // The first entry is a transfer that in nt_shared_loop would return from
2138 // thread_sched_switch0: a parked thread left its sched lock held for the
2139 // loop to release, or a terminated one (dead_co) released it itself.
2140 struct coroutine_context *dead_co = nt->dead_co;
2141 nt->dead_co = NULL;
2142 if (!thread_sched_reclaim(dead_co)) {
2143 rb_thread_t *parked_th = (rb_thread_t *)from->argument;
2144 thread_sched_unlock(TH_SCHED(parked_th), NULL);
2145 }
2146
2147 // as after a switch0 return: the thread may have pinned this nt
2148 // (rb_thread_lock_native_thread) before it ended
2149 if (!nt->dedicated) {
2150 if (nt_shared_loop(nt)) rb_bug("main nt retired"); // native_thread_self_can_retire_p() is false here
2151 }
2152 nt_snts_leave(nt->vm, nt);
2153
2154 // Went dedicated while running (rb_thread_lock_native_thread) and that
2155 // thread ended. Nothing can resume this context; sleep out the process.
2156 while (1) {
2157 pause();
2158 }
2159}
2160
2161// RUBY_MN_THREADS=2: make the running main thread an M:N thread in place.
2162// Its context becomes the process stack (as nt_start's own stack is an snt's
2163// context) and its nt joins the shared pool with a fresh stack for its loop.
2164// Nothing changes for the thread until it first blocks: that transfer
2165// starts nt_loop_co on this nt, and any snt may resume the thread later.
2166static void
2167thread_sched_main_to_shared(rb_thread_t *th)
2168{
2169 rb_vm_t *vm = th->vm;
2170 struct rb_native_thread *nt = th->nt;
2171 struct rb_thread_sched *sched = TH_SCHED(th);
2172
2173 VM_ASSERT(th == vm->ractor.main_thread);
2174 VM_ASSERT(nt->dedicated == 1 && th->has_dedicated_nt);
2175 VM_ASSERT(sched->running == th);
2176
2177 // the loop's stack (the vm stack half of the pool slot goes unused)
2178 void *vm_stack, *machine_stack;
2179 int err = nt_alloc_stack(vm, &vm_stack, &machine_stack);
2180 if (err) {
2181 rb_warn("RUBY_MN_THREADS=2: cannot allocate the main nt's stack (%s); the main thread stays dedicated", strerror(err));
2182 th->ractor->threads.sched.enable_mn_threads = true; // as =1
2183 return;
2184 }
2185 size_t machine_stack_size = vm->default_params.thread_machine_stack_size - sizeof(struct nt_machine_stack_footer);
2186 // the main nt is ZALLOC'd by Init_BareVM, not native_thread_alloc: no context yet
2187 nt->nt_context = ruby_xmalloc(sizeof(struct coroutine_context));
2188 coroutine_initialize(nt->nt_context, nt_loop_co, machine_stack, machine_stack_size);
2189 nt->nt_context->argument = nt;
2190
2191 // the thread's context is the process stack it already runs on
2192 struct rb_thread_context *tctx = ruby_xmalloc(sizeof(struct rb_thread_context));
2193 tctx->stack = NULL; // not a pool stack: never freed
2194 tctx->dead = false;
2195 tctx->nt = NULL;
2196 coroutine_initialize_main(&tctx->co);
2197 tctx->co.argument = th;
2198 th->sched.context = &tctx->co;
2199
2200 thread_sched_lock(sched, th);
2201 {
2202 // re-record the running thread as an snt's (running_dnts -> nt->running_th)
2203 thread_sched_del_running_thread(sched, th);
2204 nt->dedicated = 0;
2205 th->has_dedicated_nt = 0;
2206 nt_snts_join(vm, nt);
2207 // the pool's first snt; under the lock native_thread_check_and_create_shared
2208 // takes, so that it never mints one for the main Ractor alongside
2209 ractor_sched_lock(vm, th->ractor);
2210 {
2211 // The pool is still empty: the timer thread has been up since
2212 // Init_Thread and would mint one here, but its timeout branch is
2213 // the only path there and it sleeps untimed with nothing waiting.
2214 VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0);
2215 RUBY_ATOMIC_INC(vm->ractor.sched.snt_cnt);
2216 th->ractor->threads.sched.enable_mn_threads = true;
2217 }
2218 ractor_sched_unlock(vm, th->ractor);
2219#if USE_RUBY_DEBUG_LOG
2220 vm->ractor.sched.dnt_cnt--;
2221#endif
2222 thread_sched_add_running_thread(sched, th);
2223 }
2224 thread_sched_unlock(sched, th);
2225
2226 RUBY_DEBUG_LOG("main th:%u on nt:%d is now shared", rb_th_serial(th), nt->serial);
2227}
2228#endif
2229
2230static int native_thread_create_shared(rb_thread_t *th);
2231
2232#if USE_MN_THREADS
2233static void nt_free_stack(void *mstack);
2234
2235
2236// Reclaim the context a coroutine thread recorded in nt->dead_co before its
2237// final transfer (co_start's epilogue). Our running here proves that transfer's
2238// register save into the block completed. Returns true when a thread did
2239// terminate -- it RELEASED the sched lock before transferring; NULL/false means
2240// a live yield, where the loop still owns the lock.
2241static bool
2242thread_sched_reclaim(struct coroutine_context *dead_co)
2243{
2244 struct rb_thread_context *tctx = (struct rb_thread_context *)dead_co;
2245
2246 if (tctx != NULL && tctx->dead) {
2247 nt_free_stack(tctx->stack);
2248 SIZED_FREE(tctx);
2249 // pairs with the increment at the top of coroutine_thread_terminated:
2250 // a waiting VM destruct may proceed once this reclaim is done
2251 VM_ASSERT(RUBY_ATOMIC_LOAD(GET_VM()->ractor.sched.winding_cnt) > 0);
2252 RUBY_ATOMIC_DEC(GET_VM()->ractor.sched.winding_cnt);
2253 return true;
2254 }
2255 return false;
2256}
2257#endif
2258
2259void
2260rb_thread_wake_fence(rb_thread_t *th)
2261{
2262 timer_thread_wake_fence(th);
2263}
2264
2265void
2266rb_threadptr_sched_free(rb_thread_t *th)
2267{
2268 timer_thread_wake_fence(th);
2269#if USE_MN_THREADS
2270 // A thread with a coroutine context runs on a native thread it shares and
2271 // does not own: one made for the shared pool, or the main thread made
2272 // shared. nt->dedicated does not say so, rb_thread_lock_native_thread()
2273 // raising it on a shared native thread that stays in the pool.
2274 const bool owns_nt = th->sched.context == NULL;
2275
2276 if (th->sched.context != NULL) {
2277 // a coroutine thread that never reached its epilogue (never started),
2278 // or the main thread made shared (its stack is the process stack);
2279 // a terminated one is reclaimed by whoever resumed from its final
2280 // transfer (thread_sched_reclaim), and cleared this pointer.
2281 struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context;
2282 nt_free_stack(tctx->stack);
2283 SIZED_FREE(tctx);
2284 th->sched.context = NULL;
2285 // TODO: how to free nt and nt->altstack?
2286 }
2287 if (th->sched.malloc_stack) {
2288 SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
2289 if (th->nt && owns_nt) {
2290 native_thread_destroy(th->nt);
2291 }
2292 }
2293#else
2294 SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
2295 native_thread_destroy(th->nt);
2296#endif
2297
2298 th->nt = NULL;
2299}
2300
2301
2302static int
2303native_thread_create(rb_thread_t *th)
2304{
2305 VM_ASSERT(th->nt == 0);
2306 RUBY_DEBUG_LOG("th:%d has_dnt:%d", th->serial, th->has_dedicated_nt);
2307 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_STARTED, th);
2308
2309 if (!th->ractor->threads.sched.enable_mn_threads) {
2310 th->has_dedicated_nt = 1;
2311 }
2312
2313 if (th->has_dedicated_nt) {
2314 return native_thread_create_dedicated(th);
2315 }
2316 else {
2317 return native_thread_create_shared(th);
2318 }
2319}
2320
2321#ifdef USE_UBF_LIST
2322static CCAN_LIST_HEAD(ubf_list_head);
2323#ifdef RB_NATIVETHREAD_LOCK_INIT
2324static rb_nativethread_lock_t ubf_list_lock = RB_NATIVETHREAD_LOCK_INIT;
2325#else
2326// no static initializer on this platform; thread_sched_init_vm() does it
2327static rb_nativethread_lock_t ubf_list_lock;
2328#endif
2329
2330static void
2331ubf_list_atfork(void)
2332{
2333 ccan_list_head_init(&ubf_list_head);
2334 rb_native_mutex_initialize(&ubf_list_lock);
2335}
2336
2338static bool
2339ubf_list_contain_p(rb_thread_t *th)
2340{
2341 rb_thread_t *list_th;
2342 ccan_list_for_each(&ubf_list_head, list_th, sched.node.ubf) {
2343 if (list_th == th) return true;
2344 }
2345 return false;
2346}
2347
2348/* The thread 'th' is registered to be trying unblock. */
2349static void
2350register_ubf_list(rb_thread_t *th)
2351{
2352 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
2353 struct ccan_list_node *node = &th->sched.node.ubf;
2354
2355 VM_ASSERT(th->unblock.func != NULL);
2356
2357 rb_native_mutex_lock(&ubf_list_lock);
2358 {
2359 // check not connected yet
2360 if (ccan_list_empty((struct ccan_list_head*)node)) {
2361 VM_ASSERT(!ubf_list_contain_p(th));
2362 ccan_list_add(&ubf_list_head, node);
2363 }
2364 }
2365 rb_native_mutex_unlock(&ubf_list_lock);
2366
2367 timer_thread_wakeup();
2368}
2369
2370/* The thread 'th' is unblocked. It no longer need to be registered. */
2371static void
2372unregister_ubf_list(rb_thread_t *th)
2373{
2374 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
2375 struct ccan_list_node *node = &th->sched.node.ubf;
2376
2377 /* we can't allow re-entry into ubf_list_head */
2378 VM_ASSERT(th->unblock.func == NULL);
2379
2380 if (!ccan_list_empty((struct ccan_list_head*)node)) {
2381 rb_native_mutex_lock(&ubf_list_lock);
2382 {
2383 VM_ASSERT(ubf_list_contain_p(th));
2384 ccan_list_del_init(node);
2385 }
2386 rb_native_mutex_unlock(&ubf_list_lock);
2387 }
2388}
2389
2390/*
2391 * Poke the target thread so that it returns from a blocking syscall.
2392 * How that is done is up to the platform (native_thread_interrupt).
2393 */
2394static void
2395ubf_wakeup_thread(rb_thread_t *th)
2396{
2397 RUBY_DEBUG_LOG("th:%u thread_id:%p", rb_th_serial(th), (void *)th->nt->thread_id);
2398
2399 native_thread_interrupt(th);
2400}
2401
2402static void
2403ubf_select(void *ptr)
2404{
2405 rb_thread_t *th = (rb_thread_t *)ptr;
2406 RUBY_DEBUG_LOG("wakeup th:%u", rb_th_serial(th));
2407 ubf_wakeup_thread(th);
2408 register_ubf_list(th);
2409}
2410
2411static bool
2412ubf_threads_empty(void)
2413{
2414 return ccan_list_empty(&ubf_list_head) != 0;
2415}
2416
2417static void
2418ubf_wakeup_all_threads(void)
2419{
2420 rb_thread_t *th;
2421 rb_native_mutex_lock(&ubf_list_lock);
2422 {
2423 ccan_list_for_each(&ubf_list_head, th, sched.node.ubf) {
2424 ubf_wakeup_thread(th);
2425 }
2426 }
2427 rb_native_mutex_unlock(&ubf_list_lock);
2428}
2429
2430#else /* USE_UBF_LIST */
2431#define register_ubf_list(th) (void)(th)
2432#define unregister_ubf_list(th) (void)(th)
2433#define ubf_select 0
2434static void ubf_wakeup_all_threads(void) { return; }
2435static bool ubf_threads_empty(void) { return true; }
2436#define ubf_list_atfork() do {} while (0)
2437#endif /* USE_UBF_LIST */
2438
2439static int
2440timer_thread_set_timeout(rb_vm_t *vm)
2441{
2442#if 0
2443 return 10; // ms
2444#else
2445 int timeout = -1;
2446
2447 ractor_sched_lock(vm, NULL);
2448 {
2449 if ( timeslice_scan(vm, false) // (1-1) Provide time slice for active NTs
2450 || !ubf_threads_empty() // (1-3) Periodic UBF
2451 || vm->ractor.sched.grq_cnt > 0 // (1-4) Lazy GRQ deq start
2452 ) {
2453
2454 RUBY_DEBUG_LOG("ubf:%d grq:%d",
2455 !ubf_threads_empty(),
2456 (vm->ractor.sched.grq_cnt > 0));
2457
2458 timeout = 10; // ms
2459 vm->ractor.sched.timeslice_wait_inf = false;
2460 }
2461 else {
2462 vm->ractor.sched.timeslice_wait_inf = true;
2463 }
2464 }
2465 ractor_sched_unlock(vm, NULL);
2466
2467 timeout = timer_wheel_timeout(timeout);
2468
2469 RUBY_DEBUG_LOG("timeout:%d inf:%d", timeout, (int)vm->ractor.sched.timeslice_wait_inf);
2470
2471 // fprintf(stderr, "timeout:%d\n", timeout);
2472 return timeout;
2473#endif
2474}
2475
2476static void
2477timer_thread_check_signal(rb_vm_t *vm)
2478{
2479 // ruby_sigchld_handler(vm); TODO
2480
2481 int signum = rb_signal_buff_size();
2482 if (UNLIKELY(signum > 0) && vm->ractor.main_thread) {
2483 RUBY_DEBUG_LOG("signum:%d", signum);
2484 threadptr_trap_interrupt(vm->ractor.main_thread);
2485 }
2486}
2487
2488// Tick (with `interrupt`) each listed sched's running thread and prune scheds
2489// whose readyq drained; returns whether any sched still needs ticks.
2490static bool
2491timeslice_scan(rb_vm_t *vm, bool interrupt)
2492{
2493 bool found = false;
2494 struct rb_thread_sched *sched, *next;
2495
2496 rb_native_mutex_lock(&vm->ractor.sched.timeslice.lock);
2497 {
2498 ccan_list_for_each_safe(&vm->ractor.sched.timeslice.scheds, sched, next, timeslice_node) {
2499 // trylock: timeslice_sched_link nests sched.lock -> timeslice.lock,
2500 // this scan holds the locks the other way around
2501 if (rb_native_mutex_trylock(&sched->lock_) == 0) {
2502 if (ccan_list_empty(&sched->readyq)) {
2503 ccan_list_del_init(&sched->timeslice_node); // a later enq relinks it
2504 }
2505 else if (sched->is_running) {
2506 VM_ASSERT(sched->running != NULL);
2507 found = true;
2508 if (interrupt) {
2509 RUBY_DEBUG_LOG("timeslice th:%u", rb_th_serial(sched->running));
2510 RUBY_VM_SET_TIMER_INTERRUPT(sched->running->ec);
2511 }
2512 }
2513 // else: waiters behind a blocked runner need no ticks; the
2514 // add path wakes the timer when the sched runs again
2515 rb_native_mutex_unlock(&sched->lock_);
2516 }
2517 else {
2518 found = true; // busy switching; tick it on the next round
2519 }
2520 }
2521 }
2522 rb_native_mutex_unlock(&vm->ractor.sched.timeslice.lock);
2523
2524 return found;
2525}
2526
2527static void
2528timer_thread_check_timeslice(rb_vm_t *vm)
2529{
2530 // TODO: check time
2531 timeslice_scan(vm, true);
2532}
2533
2534static void *
2535timer_thread_func(void *ptr)
2536{
2537 rb_vm_t *vm = (rb_vm_t *)ptr;
2538#if defined(RUBY_NT_SERIAL)
2539 ruby_nt_serial = (rb_atomic_t)-1;
2540#endif
2541
2542 RUBY_DEBUG_LOG("started%s", "");
2543
2544 while (RUBY_ATOMIC_LOAD(system_working)) {
2545 timer_thread_check_signal(vm);
2546 timer_thread_check_timeout(vm);
2547 ubf_wakeup_all_threads();
2548
2549 RUBY_DEBUG_LOG("system_working:%d", RUBY_ATOMIC_LOAD(system_working));
2550 timer_thread_polling(vm);
2551 }
2552
2553 RUBY_DEBUG_LOG("terminated");
2554 return NULL;
2555}
2556
2557static void
2558timer_thread_wakeup_locked(rb_vm_t *vm)
2559{
2560 // should be locked before.
2561 ASSERT_ractor_sched_locked(vm, NULL);
2562
2563 if (TIMER_THREAD_CREATED_P()) {
2564 if (vm->ractor.sched.timeslice_wait_inf) {
2565 RUBY_DEBUG_LOG("wakeup%s", "");
2566 timer_thread_wakeup_force();
2567 }
2568 else {
2569 RUBY_DEBUG_LOG("will be wakeup...");
2570 }
2571 }
2572}
2573
2574static void
2575timer_thread_wakeup(void)
2576{
2577 rb_vm_t *vm = GET_VM();
2578
2579 ractor_sched_lock(vm, NULL);
2580 {
2581 timer_thread_wakeup_locked(vm);
2582 }
2583 ractor_sched_unlock(vm, NULL);
2584}
2585
2586static void
2587native_sleep(rb_thread_t *th, rb_hrtime_t *rel)
2588{
2589 struct rb_thread_sched *sched = TH_SCHED(th);
2590
2591 RUBY_DEBUG_LOG("rel:%d", rel ? (int)*rel : 0);
2592
2593 if (rel && !th_has_dedicated_nt(th)) {
2594 // an M:N thread has no condvar of its own: the timer thread wakes it
2595 thread_sched_wait_events(sched, th, -1, thread_sched_waiting_timeout, rel);
2596 }
2597 else if (rel) {
2598 /* Solaris cond_timedwait() returns EINVAL if an argument is greater than
2599 * current_time + 100,000,000. So cut up to 100,000,000. This is
2600 * considered as a kind of spurious wakeup. The caller to native_sleep
2601 * should care about spurious wakeup.
2602 *
2603 * See also [Bug #1341] [ruby-core:29702]
2604 * http://download.oracle.com/docs/cd/E19683-01/816-0216/6m6ngupgv/index.html
2605 */
2606 const rb_hrtime_t max = (rb_hrtime_t)100000000 * RB_HRTIME_PER_SEC;
2607 if (*rel > max) *rel = max;
2608
2609 rb_hrtime_t end = rb_hrtime_add(rb_hrtime_now(), *rel);
2610 thread_sched_to_waiting_until_wakeup(sched, th, &end);
2611 }
2612 else {
2613 thread_sched_to_waiting_until_wakeup(sched, th, NULL);
2614 }
2615
2616 RUBY_DEBUG_LOG("wakeup");
2617}
2618
2619
2620// return true if the current thread acquires DNT.
2621// return false if the current thread already acquires DNT.
2622bool
2624{
2625 rb_thread_t *th = GET_THREAD();
2626 bool is_snt = th->nt->dedicated == 0;
2627 native_thread_dedicated_inc(th->vm, th->ractor, th->nt);
2628
2629 return is_snt;
2630}
2631
2632// rb_ractor_terminate_all() waits for the Ractors it interrupted on a condvar
2633// of its own, holding the VM lock, which it drops for the wait and takes back
2634// after. An M:N thread would hold the shared native thread it runs on for the
2635// whole wait as well, leaving those Ractors with nothing to run on, so give
2636// that native thread back the way a blocking region does.
2637void
2638rb_ractor_sched_wait_terminate(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
2639{
2640 ASSERT_vm_locking();
2641
2642 rb_thread_t *th = GET_THREAD();
2643 unsigned int lock_rec = vm->ractor.sync.lock_rec;
2644 rb_ractor_t *lock_owner = vm->ractor.sync.lock_owner;
2645
2646 vm->ractor.sync.lock_rec = 0;
2647 vm->ractor.sync.lock_owner = NULL;
2648
2649 native_thread_dedicated_inc(vm, th->ractor, th->nt);
2650 rb_native_cond_timedwait(cond, &vm->ractor.sync.lock, msec);
2651 native_thread_dedicated_dec(vm, th->ractor, th->nt);
2652
2653 vm->ractor.sync.lock_rec = lock_rec;
2654 vm->ractor.sync.lock_owner = lock_owner;
2655}
2656
2657void
2658rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)
2659{
2660 th->sched.malloc_stack = true;
2661 th->sched.context_stack = stack;
2662 th->sched.context_stack_size = stack_size;
2663}
2664
2665// VM wide scheduler state, shared by every platform. Called from
2666// Init_native_thread() before the main thread is recorded.
2667static void
2668thread_sched_init_vm(rb_vm_t *vm)
2669{
2670 rb_native_mutex_initialize(&vm->ractor.sched.lock);
2671 rb_native_cond_initialize(&vm->ractor.sched.cond);
2672 rb_native_cond_initialize(&vm->ractor.sched.barrier_complete_cond);
2673 rb_native_cond_initialize(&vm->ractor.sched.barrier_release_cond);
2674
2675 ccan_list_head_init(&vm->ractor.sched.grq);
2676 rb_native_mutex_initialize(&vm->ractor.sched.ntlist.lock);
2677 ccan_list_head_init(&vm->ractor.sched.ntlist.running_dnts);
2678 ccan_list_head_init(&vm->ractor.sched.ntlist.snts);
2679 rb_native_mutex_initialize(&vm->ractor.sched.timeslice.lock);
2680 ccan_list_head_init(&vm->ractor.sched.timeslice.scheds);
2681
2682#ifndef RB_NATIVETHREAD_LOCK_INIT
2683 // ubf_list_lock could not be initialized statically
2684 ubf_list_atfork();
2685#endif
2686}
#define RUBY_ATOMIC_INC(var)
Atomically increments the value pointed by var.
Definition atomic.h:214
#define RUBY_ATOMIC_CAS(var, oldval, newval)
Atomic compare-and-swap.
Definition atomic.h:165
std::atomic< unsigned > rb_atomic_t
Type that is eligible for atomic operations.
Definition atomic.h:69
#define RUBY_ATOMIC_FETCH_ADD(var, val)
Atomically replaces the value pointed by var with the result of addition of val to the old value of v...
Definition atomic.h:118
#define RUBY_ATOMIC_ADD(var, val)
Identical to RUBY_ATOMIC_FETCH_ADD, except for the return type.
Definition atomic.h:195
#define RUBY_ATOMIC_FETCH_SUB(var, val)
Atomically replaces the value pointed by var with the result of subtraction of val to the old value o...
Definition atomic.h:129
#define RUBY_ATOMIC_DEC(var)
Atomically decrements the value pointed by var.
Definition atomic.h:223
#define RUBY_ATOMIC_LOAD(var)
Atomic load.
Definition atomic.h:175
#define RUBY_ATOMIC_SET(var, val)
Identical to RUBY_ATOMIC_EXCHANGE, except for the return type.
Definition atomic.h:185
void rb_warn(const char *fmt,...)
Identical to rb_warning(), except it reports unless $VERBOSE is nil.
Definition error.c:468
void rb_unblock_function_t(void *)
This is the type of UBFs.
Definition thread.h:336
#define RUBY_INTERNAL_THREAD_EVENT_RESUMED
Triggered when a thread successfully acquired the GVL.
Definition thread.h:249
#define RUBY_INTERNAL_THREAD_EVENT_EXITED
Triggered when a thread exits.
Definition thread.h:263
#define RUBY_INTERNAL_THREAD_EVENT_SUSPENDED
Triggered when a thread released the GVL.
Definition thread.h:256
bool rb_thread_lock_native_thread(void)
Declare the current Ruby thread should acquire a dedicated native thread on M:N thread scheduler.
#define RUBY_INTERNAL_THREAD_EVENT_STARTED
Triggered when a new thread is started.
Definition thread.h:235
#define RUBY_INTERNAL_THREAD_EVENT_READY
Triggered when a thread attempt to acquire the GVL.
Definition thread.h:242
#define RBIMPL_ATTR_MAYBE_UNUSED()
Wraps (or simulates) [[maybe_unused]]
void rb_native_mutex_lock(rb_nativethread_lock_t *lock)
Just another name of rb_nativethread_lock_lock.
void rb_native_cond_initialize(rb_nativethread_cond_t *cond)
Fills the passed condition variable with an initial value.
int rb_native_mutex_trylock(rb_nativethread_lock_t *lock)
Identical to rb_native_mutex_lock(), except it doesn't block in case rb_native_mutex_lock() would.
void rb_native_cond_broadcast(rb_nativethread_cond_t *cond)
Signals a condition variable.
void rb_native_mutex_initialize(rb_nativethread_lock_t *lock)
Just another name of rb_nativethread_lock_initialize.
void rb_native_mutex_unlock(rb_nativethread_lock_t *lock)
Just another name of rb_nativethread_lock_unlock.
void rb_native_cond_signal(rb_nativethread_cond_t *cond)
Signals a condition variable.
void rb_native_cond_wait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex)
Waits for the passed condition variable to be signalled.
void rb_native_cond_timedwait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex, unsigned long msec)
Identical to rb_native_cond_wait(), except it additionally takes timeout in msec resolution.
uintptr_t VALUE
Type that represents a Ruby object.
Definition value.h:40