Ruby 4.1.0dev (2026-11-12 revision e6e232d5facd495b6a7f143cf6e9c7960392ca25)
thread_sched.c (e6e232d5facd495b6a7f143cf6e9c7960392ca25)
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 // also on the M:N path and when the waker already made th running
870 rb_ractor_thread_switch(th->ractor, th, false);
871
872
873 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_RESUMED, th);
874}
875
876// waiting -> ready -> running (locked)
877static void
878thread_sched_to_running_common(struct rb_thread_sched *sched, rb_thread_t *th)
879{
880 RUBY_DEBUG_LOG("th:%u dedicated:%d", rb_th_serial(th), th_has_dedicated_nt(th));
881
882 VM_ASSERT(sched->running != th);
883 VM_ASSERT(th_has_dedicated_nt(th));
884 VM_ASSERT(GET_THREAD() == th);
885
886 native_thread_dedicated_dec(th->vm, th->ractor, th->nt);
887
888 // waiting -> ready
889 thread_sched_to_ready_common(sched, th, false, false);
890
891 if (sched->running == th) {
892 thread_sched_add_running_thread(sched, th);
893 }
894
895 // TODO: check SNT number
896 thread_sched_wait_running_turn(sched, th, false, NULL);
897}
898
899// waiting -> ready -> running
900//
901// `th` had been waiting by `thread_sched_to_waiting()`
902// and run a dedicated task (like waitpid and so on).
903// After the dedicated task, this function is called
904// to join a normal thread-scheduling.
905static void
906thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th)
907{
908 // We are reading and writing these sched fields without lock cover, but
909 // there are no correctness issues resulting from stale cache or delayed writeback.
910 // When it works, this causes the next-scheduled thread to yield the sched lock
911 // briefly so that we can grab it if we're still spinning (not descheduled yet).
912 if (sched->runnable_hot_th == th) {
913 sched->runnable_hot_th_waiting = 1;
914 }
915 thread_sched_lock(sched, th);
916 {
917 thread_sched_to_running_common(sched, th);
918 }
919 thread_sched_unlock(sched, th);
920}
921
922// resume a next thread in the thread ready queue.
923//
924// deque next running thread from the ready thread queue and
925// resume this thread if available.
926//
927// If the next therad has a dedicated native thraed, simply signal to resume.
928// Otherwise, make the ractor ready and other nt will run the ractor and the thread.
929static void
930thread_sched_wakeup_next_thread(struct rb_thread_sched *sched, rb_thread_t *th, bool will_switch)
931{
932 ASSERT_thread_sched_locked(sched, th);
933
934 VM_ASSERT(sched->running == th);
935 VM_ASSERT(sched->running->nt != NULL);
936
937 rb_thread_t *next_th = thread_sched_deq(sched);
938
939 RUBY_DEBUG_LOG("next_th:%u", rb_th_serial(next_th));
940 VM_ASSERT(th != next_th);
941
942 thread_sched_set_running(sched, next_th);
943 VM_ASSERT(next_th == sched->running);
944 thread_sched_wakeup_running_thread(sched, next_th, will_switch);
945
946 if (th != next_th) {
947 thread_sched_del_running_thread(sched, th);
948 }
949}
950
951// running -> dead (locked)
952static void
953thread_sched_to_dead_common(struct rb_thread_sched *sched, rb_thread_t *th)
954{
955 RUBY_DEBUG_LOG("th:%u DNT:%d", rb_th_serial(th), th->nt->dedicated);
956
957 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
958
959 // A dying coroutine thread (will_switch=true here) does NOT wake the
960 // next thread now: it is still winding down (co_start's epilogue), and
961 // the same Ractor must not have two threads executing at once. The
962 // epilogue enqueues the Ractor after its last rb_ractor_t access.
963 thread_sched_wakeup_next_thread(sched, th, !th_has_dedicated_nt(th));
964
965 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_EXITED, th);
966}
967
968// running -> dead
969static void
970thread_sched_to_dead(struct rb_thread_sched *sched, rb_thread_t *th)
971{
972 // wait out any pending wake here, while th's Ractor is still alive
973 timer_thread_wake_fence(th);
974
975 thread_sched_lock(sched, th);
976 {
977 thread_sched_to_dead_common(sched, th);
978 }
979 thread_sched_unlock(sched, th);
980}
981
982// running -> waiting (locked)
983//
984// This thread will run dedicated task (th->nt->dedicated++).
985static void
986thread_sched_to_waiting_common(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately)
987{
988 RUBY_DEBUG_LOG("th:%u DNT:%d", rb_th_serial(th), th->nt->dedicated);
989
990 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
991
992 native_thread_dedicated_inc(th->vm, th->ractor, th->nt);
993 if (!yield_immediately) {
994 sched->runnable_hot_th = th;
995 sched->runnable_hot_th_waiting = 0;
996 }
997 thread_sched_wakeup_next_thread(sched, th, false);
998}
999
1000// running -> waiting
1001//
1002// This thread will run a dedicated task.
1003static void
1004thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately)
1005{
1006 thread_sched_lock(sched, th);
1007 {
1008 thread_sched_to_waiting_common(sched, th, yield_immediately);
1009 }
1010 thread_sched_unlock(sched, th);
1011}
1012
1013// mini utility func
1014// return true if any there are any interrupts
1015static bool
1016ubf_set(rb_thread_t *th, rb_unblock_function_t *func, void *arg, rb_atomic_t *event_serial)
1017{
1018 VM_ASSERT(func != NULL);
1019
1020 retry:
1021 if (RUBY_VM_INTERRUPTED(th->ec)) {
1022 RUBY_DEBUG_LOG("interrupted:0x%x", th->ec->interrupt_flag);
1023 return true;
1024 }
1025
1026 rb_native_mutex_lock(&th->interrupt_lock);
1027 {
1028 if (!th->ec->raised_flag && RUBY_VM_INTERRUPTED(th->ec)) {
1029 rb_native_mutex_unlock(&th->interrupt_lock);
1030 goto retry;
1031 }
1032
1033 VM_ASSERT(th->unblock.func == NULL);
1034 th->unblock.func = func;
1035 th->unblock.arg = arg;
1036 if (event_serial) {
1037 rb_atomic_t prev_serial = RUBY_ATOMIC_FETCH_ADD(th->unblock.event_serial, 1);
1038 *event_serial = prev_serial+1;
1039 }
1040 }
1041 rb_native_mutex_unlock(&th->interrupt_lock);
1042
1043 return false;
1044}
1045
1046static void
1047ubf_clear(rb_thread_t *th, bool clear_serial)
1048{
1049 rb_native_mutex_lock(&th->interrupt_lock);
1050 {
1051 th->unblock.func = NULL;
1052 th->unblock.arg = NULL;
1053 if (clear_serial) {
1054 RUBY_ATOMIC_ADD(th->unblock.event_serial, 1);
1055 }
1056 }
1057 rb_native_mutex_unlock(&th->interrupt_lock);
1058}
1059
1060static void
1061ubf_waiting(void *ptr)
1062{
1063 rb_thread_t *th = (rb_thread_t *)ptr;
1064 struct rb_thread_sched *sched = TH_SCHED(th);
1065
1066 // only once. it is safe because th->interrupt_lock is already acquired.
1067 th->unblock.func = NULL;
1068 th->unblock.arg = NULL;
1069
1070 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
1071
1072 thread_sched_lock(sched, th);
1073 {
1074 if (sched->running == th || th->sched.node.is_ready) {
1075 // not sleeping yet, or a deadline already put it back in line
1076 }
1077 else {
1078 thread_sched_to_ready_common(sched, th, true, false);
1079
1080 // If the turn is taken, th stays parked until the running thread yields.
1081 // For a timed wait, wake it early anyway: it re-parks at once, but its
1082 // wakeup then runs on another core in parallel with the running thread,
1083 // off the handoff path. An untimed wait has no post-wake bookkeeping
1084 // worth pipelining, so it skips the extra futex round.
1085 if (sched->running != th && th->sched.waiting_timed &&
1086 th->nt != NULL && th_has_dedicated_nt(th)) {
1087 rb_native_cond_signal(&th->nt->readyq);
1088 }
1089 }
1090 }
1091 thread_sched_unlock(sched, th);
1092}
1093
1094// running -> waiting
1095//
1096// This thread will sleep until other thread wakeup the thread. `end` is an
1097// absolute deadline, NULL to sleep until woken; only a dedicated native thread,
1098// which parks on its own condvar, can take one.
1099static void
1100thread_sched_to_waiting_until_wakeup(struct rb_thread_sched *sched, rb_thread_t *th, const rb_hrtime_t *end)
1101{
1102 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
1103
1104 VM_ASSERT(end == NULL || th_has_dedicated_nt(th));
1105
1106 RB_VM_SAVE_MACHINE_CONTEXT(th);
1107
1108
1109 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
1110
1111 thread_sched_lock(sched, th);
1112 {
1113 // NOTE: there's a lock ordering inversion here with the ubf call, but it's benign.
1114 if (ubf_set(th, ubf_waiting, (void *)th, NULL)) {
1115 RUBY_DEBUG_LOG("th:%u interrupted", rb_th_serial(th));
1116 }
1117 else {
1118 bool can_direct_transfer = !th_has_dedicated_nt(th);
1119 th->sched.waiting_timed = (end != NULL); // never true here for M:N (end is NULL)
1120 // NOTE: th->status is set before and after this sleep outside of this function in `sleep_forever`
1121 thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
1122 thread_sched_wait_running_turn(sched, th, can_direct_transfer, end);
1123 th->sched.waiting_timed = false;
1124 }
1125 }
1126 thread_sched_unlock(sched, th);
1127
1128 ubf_clear(th, false);
1129}
1130
1131// run another thread in the ready queue.
1132// continue to run if there are no ready threads.
1133static void
1134thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th)
1135{
1136 RUBY_DEBUG_LOG("th:%d sched->readyq_cnt:%d", (int)th->serial, sched->readyq_cnt);
1137
1138 thread_sched_lock(sched, th);
1139 {
1140 if (!ccan_list_empty(&sched->readyq)) {
1141 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
1142 thread_sched_wakeup_next_thread(sched, th, !th_has_dedicated_nt(th));
1143 bool can_direct_transfer = !th_has_dedicated_nt(th);
1144 thread_sched_to_ready_common(sched, th, false, can_direct_transfer);
1145 thread_sched_wait_running_turn(sched, th, can_direct_transfer, NULL);
1146 th->status = THREAD_RUNNABLE;
1147 }
1148 else {
1149 VM_ASSERT(sched->readyq_cnt == 0);
1150 }
1151 }
1152 thread_sched_unlock(sched, th);
1153}
1154
1155void
1156rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
1157{
1158 rb_native_mutex_initialize(&sched->lock_);
1159
1160#if VM_CHECK_MODE
1161 sched->lock_owner = NULL;
1162#endif
1163
1164 ccan_list_head_init(&sched->readyq);
1165 sched->readyq_cnt = 0;
1166 ccan_list_node_init(&sched->grq_node); // self-linked = not enqueued
1167 ccan_list_node_init(&sched->timeslice_node);
1168
1169#if USE_MN_THREADS
1170 // A Ractor's threads are M:N unless RUBY_MN_THREADS turns it off entirely;
1171 // the main Ractor's setting is decided in ruby_mn_threads_params().
1172 if (!atfork) sched->enable_mn_threads = mn_threads_enabled_p();
1173#endif
1174}
1175
1176static void
1177coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_context *transfer_to, bool to_dead)
1178{
1179#ifdef RUBY_ASAN_ENABLED
1180 void **fake_stack = to_dead ? NULL : &transfer_from->fake_stack;
1181 __sanitizer_start_switch_fiber(fake_stack, transfer_to->stack_base, transfer_to->stack_size);
1182#endif
1183
1184#if defined(COROUTINE_SANITIZE_THREAD)
1185 /* Tell TSan we are switching to transfer_to's fiber before the stack
1186 * switch, so its per-thread shadow stack stays bound to the right
1187 * coroutine. */
1188 __tsan_switch_to_fiber(transfer_to->tsan_fiber, 0);
1189#endif
1190
1192 struct coroutine_context *returning_from = coroutine_transfer(transfer_from, transfer_to);
1193
1194 /* if to_dead was passed, the caller is promising that this coroutine is finished and it should
1195 * never be resumed! */
1196 VM_ASSERT(!to_dead);
1197#ifdef RUBY_ASAN_ENABLED
1198 __sanitizer_finish_switch_fiber(transfer_from->fake_stack,
1199 (const void**)&returning_from->stack_base, &returning_from->stack_size);
1200#endif
1201}
1202
1203static void
1204thread_sched_switch0(struct coroutine_context *current_cont, rb_thread_t *next_th, struct rb_native_thread *nt, bool to_dead)
1205{
1206 VM_ASSERT(!nt->dedicated);
1207 VM_ASSERT(next_th->nt == NULL);
1208
1209 RUBY_DEBUG_LOG("next_th:%u", rb_th_serial(next_th));
1210
1211 // this direct transfer serves next_th without a dequeue; cancel its
1212 // Ractor's outstanding grq entry (no-op when nothing is enqueued)
1213 ractor_sched_cancel_enq(next_th->vm, TH_SCHED(next_th));
1214
1215 ruby_thread_set_native(next_th);
1216 native_thread_assign(nt, next_th);
1217
1218 coroutine_transfer0(current_cont, next_th->sched.context, to_dead);
1219}
1220
1221static void
1222thread_sched_switch(rb_thread_t *cth, rb_thread_t *next_th)
1223{
1224 struct rb_native_thread *nt = cth->nt;
1225 native_thread_assign(NULL, cth);
1226 RUBY_DEBUG_LOG("th:%u->%u on nt:%d", rb_th_serial(cth), rb_th_serial(next_th), nt->serial);
1227 // never final: a thread ends only through co_start's epilogue transfer. The
1228 // main thread is THREAD_KILLED (rb_ec_cleanup) while it still parks here.
1229 thread_sched_switch0(cth->sched.context, next_th, nt, false);
1230}
1231
1232#if VM_CHECK_MODE > 0
1234static unsigned int
1235grq_size(rb_vm_t *vm, rb_ractor_t *cr)
1236{
1237 ASSERT_ractor_sched_locked(vm, cr);
1238
1239 rb_ractor_t *r, *prev_r = NULL;
1240 unsigned int i = 0;
1241
1242 ccan_list_for_each(&vm->ractor.sched.grq, r, threads.sched.grq_node) {
1243 i++;
1244
1245 VM_ASSERT(r != prev_r);
1246 prev_r = r;
1247 }
1248 return i;
1249}
1250#endif
1251
1252// A native thread enters/leaves an epilogue that outlives its Ractor: from
1253// the increment until the decrement, ruby_vm_destruct waits for it below.
1254// The increment must happen while the VM still counts the thread's Ractor,
1255// so that the two never look absent at the same time.
1256void
1257rb_thread_sched_winding_begin(rb_vm_t *vm)
1258{
1259 RUBY_ATOMIC_INC(vm->ractor.sched.winding_cnt);
1260}
1261
1262void
1263rb_thread_sched_winding_end(rb_vm_t *vm)
1264{
1265 VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.winding_cnt) > 0);
1266 RUBY_ATOMIC_DEC(vm->ractor.sched.winding_cnt);
1267}
1268
1269// ruby_vm_destruct: wait until no native thread is between a coroutine
1270// epilogue and its reclaim -- past that point the reclaim frees through the
1271// (about to be destroyed) objspace and reads the (about to be unset) VM.
1272// Runs without the VM lock, which the epilogue needs to progress.
1273void
1274rb_thread_sched_wait_winding(rb_vm_t *vm)
1275{
1276 while (RUBY_ATOMIC_LOAD(vm->ractor.sched.winding_cnt) > 0) {
1277 native_thread_yield();
1278 }
1279}
1280
1281// A direct service of a runnable thread (direct transfer or the hot-thread
1282// steal) bypasses the grq; cancel the Ractor's outstanding entry so that
1283// "enqueued <=> runnable and unserved" keeps holding. The caller holds the
1284// per-Ractor sched lock, so no concurrent enqueue can relink the node: a
1285// self-linked read needs no lock (the common case -- direct switches whose
1286// transition never enqueued). A linked read can race only with a dequeue,
1287// hence the recheck under the grq lock.
1288static void
1289ractor_sched_cancel_enq(rb_vm_t *vm, struct rb_thread_sched *sched)
1290{
1291 if (sched->grq_node.next != &sched->grq_node) {
1292 ractor_sched_lock(vm, NULL);
1293 {
1294 if (sched->grq_node.next != &sched->grq_node) {
1295 ccan_list_del_init(&sched->grq_node);
1296 VM_ASSERT(vm->ractor.sched.grq_cnt > 0);
1297 vm->ractor.sched.grq_cnt--;
1298 }
1299 }
1300 ractor_sched_unlock(vm, NULL);
1301 }
1302}
1303
1304static void
1305ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r)
1306{
1307 struct rb_thread_sched *sched = &r->threads.sched;
1308 rb_ractor_t *cr = NULL; // timer thread can call this function
1309
1310 VM_ASSERT(sched->running != NULL);
1311 VM_ASSERT(sched->running->nt == NULL);
1312
1313 ractor_sched_lock(vm, cr);
1314 {
1315 // Precondition: not already enqueued (the grq_node is self-linked).
1316 // This holds because every service of a runnable-but-unserved thread
1317 // either dequeues the entry (the nt scheduling loop) or cancels it
1318 // (direct transfers / the hot-thread steal; see
1319 // ractor_sched_cancel_enq) -- re-adding a linked node would corrupt
1320 // the queue, so check unconditionally (a CHECK-mode-only assert
1321 // would miss it: the race needs timing that CHECK builds perturb).
1322 if (sched->grq_node.next != &sched->grq_node) {
1323 rb_bug("ractor_sched_enq: already enqueued");
1324 }
1325 ccan_list_add_tail(&vm->ractor.sched.grq, &sched->grq_node);
1326 vm->ractor.sched.grq_cnt++;
1327 VM_ASSERT(grq_size(vm, cr) == vm->ractor.sched.grq_cnt);
1328
1329 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);
1330
1331 rb_native_cond_signal(&vm->ractor.sched.cond);
1332
1333 // The signal reaches a parked snt, and a running one revisits the
1334 // queue in ractor_sched_deq before it can wait (same lock as here).
1335 // With every snt dedicated or retired, only the timer thread's
1336 // timeout branch can serve the entry or widen the pool: wake it
1337 // (a no-op unless it sleeps untimed).
1338 if (RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0) {
1339 timer_thread_wakeup_locked(vm);
1340 }
1341
1342 // ractor_sched_dump(vm);
1343 }
1344 ractor_sched_unlock(vm, cr);
1345}
1346
1347
1348#ifndef MINIMUM_SNT
1349// make at least MINIMUM_SNT snts for debug.
1350#define MINIMUM_SNT 0
1351#endif
1352
1353/* A shared thread woken with nothing to run is one whose turn another thread
1354 * took first. After this many in a row it gives itself back: the queue keeps
1355 * running dry, so the pool is wider than the work. 0 retires on the first one
1356 * and is too eager to be useful; a negative value keeps every thread. */
1357#ifndef SNT_IDLE_RETIRE
1358#define SNT_IDLE_RETIRE 3
1359#endif
1360
1361/* Never give the last shared thread back. With none left an enqueue has nobody
1362 * to signal, and the only code that makes one runs on the timer thread's
1363 * timeout branch, which is reached only once it has seen a backlog. */
1364#define SNT_KEEP_MINIMUM (MINIMUM_SNT > 1 ? MINIMUM_SNT : 1)
1365
1366static rb_ractor_t *
1367ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr, bool can_retire)
1368{
1369 rb_ractor_t *r;
1370 int idle_streak = 0; // consecutive pops that found the queue empty
1371
1372 ractor_sched_lock(vm, cr);
1373 {
1374 RUBY_DEBUG_LOG("empty? %d", ccan_list_empty(&vm->ractor.sched.grq));
1375 // ractor_sched_dump(vm);
1376
1377 VM_ASSERT(rb_current_execution_context(false) == NULL);
1378 VM_ASSERT(grq_size(vm, cr) == vm->ractor.sched.grq_cnt);
1379
1380 while ((r = ccan_list_pop(&vm->ractor.sched.grq, rb_ractor_t, threads.sched.grq_node)) == NULL) {
1381 RUBY_DEBUG_LOG("wait grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
1382
1383 if (can_retire && SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
1384 (int)RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) > SNT_KEEP_MINIMUM) {
1385 RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
1386 RUBY_DEBUG_LOG("retire, snt_cnt:%d", (int)vm->ractor.sched.snt_cnt);
1387 break; // returning NULL ends this nt; see the caller
1388 }
1389
1390 ractor_sched_set_unlocked(vm, cr);
1391 rb_native_cond_wait(&vm->ractor.sched.cond, &vm->ractor.sched.lock);
1392 ractor_sched_set_locked(vm, cr);
1393
1394 RUBY_DEBUG_LOG("wakeup grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
1395 }
1396
1397 VM_ASSERT(rb_current_execution_context(false) == NULL);
1398
1399 if (r) {
1400 ccan_list_node_init(&r->threads.sched.grq_node); // back to self-linked
1401 VM_ASSERT(vm->ractor.sched.grq_cnt > 0);
1402 vm->ractor.sched.grq_cnt--;
1403 RUBY_DEBUG_LOG("r:%d grq_cnt:%u", (int)rb_ractor_id(r), vm->ractor.sched.grq_cnt);
1404 }
1405 else {
1406 // the retire branch is the only way out of the loop without a ractor
1407 VM_ASSERT(idle_streak > SNT_IDLE_RETIRE);
1408 }
1409 }
1410 ractor_sched_unlock(vm, cr);
1411
1412 return r;
1413}
1414
1415void rb_ractor_lock_self(rb_ractor_t *r);
1416void rb_ractor_unlock_self(rb_ractor_t *r);
1417
1418// The current thread for a ractor is put to "sleep" (descheduled in the STOPPED_FOREVER state) waiting for
1419// a ractor action to wake it up.
1420void
1421rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ubf_arg)
1422{
1423 // ractor lock of cr is acquired
1424
1425 RUBY_DEBUG_LOG("start%s", "");
1426
1427 rb_thread_t * volatile th = rb_ec_thread_ptr(ec);
1428 struct rb_thread_sched *sched = TH_SCHED(th);
1429 struct ractor_waiter *waiter = (struct ractor_waiter*)ubf_arg;
1430
1431 if (ubf_set(th, ubf, ubf_arg, &waiter->event_serial)) {
1432 // interrupted
1433 return;
1434 }
1435
1436 thread_sched_lock(sched, th);
1437 rb_ractor_unlock_self(cr);
1438 {
1439 // A dedicated native thread takes the deadline on the very condvar a wakeup
1440 // signals. An M:N thread has no condvar of its own, so its deadline goes to
1441 // the timer thread, which then wakes it the way rb_ractor_sched_wakeup() does.
1442 bool dedicated = th_has_dedicated_nt(th);
1443 const rb_hrtime_t *end_p = NULL;
1444 bool armed = false, expired = false;
1445
1446 if (waiter->end) {
1447 if (dedicated) {
1448 end_p = waiter->end;
1449 }
1450 else {
1451 // the timer wheel takes a relative timeout
1452 rb_hrtime_t now = rb_hrtime_now();
1453 rb_hrtime_t rel = *waiter->end > now ? *waiter->end - now : 0;
1454
1455 armed = ractor_sched_timeout_arm(th, &rel);
1456 expired = !armed;
1457 }
1458 }
1459
1460 if (expired) {
1461 RUBY_DEBUG_LOG("expired before sleep%s", "");
1462 }
1463 else if (armed && th->sched.waiting_reason.flags == thread_sched_waiting_none) {
1464 // the timer thread already took this thread out of the wheel; bump the
1465 // serial so that it does not try to wake a thread that never slept
1466 th->sched.event_serial++;
1467 }
1468 else {
1469 // setup sleep
1470 bool can_direct_transfer = !dedicated;
1471 RB_VM_SAVE_MACHINE_CONTEXT(th);
1472 th->status = THREAD_STOPPED_FOREVER;
1473 th->sched.waiting_timed = (end_p != NULL); // never true here for M:N (end_p is NULL)
1474 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
1475 thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
1476 // sleep
1477 thread_sched_wait_running_turn(sched, th, can_direct_transfer, end_p);
1478 th->sched.waiting_timed = false;
1479 th->status = THREAD_RUNNABLE;
1480
1481 // whoever woke this thread took the timeout back first
1482 VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
1483 }
1484 }
1485 thread_sched_unlock(sched, th);
1486 rb_ractor_lock_self(cr);
1487
1488 ubf_clear(th, true);
1489
1490 RUBY_DEBUG_LOG("end%s", "");
1491}
1492
1493void
1494rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *r_th)
1495{
1496 // ractor lock of r acquired
1497 struct rb_thread_sched *sched = TH_SCHED(r_th);
1498
1499 RUBY_DEBUG_LOG("r:%u th:%d", (unsigned int)rb_ractor_id(r), r_th->serial);
1500
1501 thread_sched_lock(sched, r_th);
1502 {
1503 if (r_th->status == THREAD_STOPPED_FOREVER) {
1504 RUBY_ATOMIC_ADD(r_th->unblock.event_serial, 1);
1505
1506 // r_th must not resume with a wheel entry left behind: take its timeout
1507 // back, as ubf_event_waiting() does. Only r_th arms it, and it is
1508 // parked here, so reading the flags without the timer lock is safe.
1509 if (r_th->sched.waiting_reason.flags != thread_sched_waiting_none) {
1510 ractor_sched_timeout_disarm(r_th);
1511 }
1512
1513 // a timeout that fired first may have made r_th runnable already: waking
1514 // it twice would put it on the readyq twice
1515 if (sched->running != r_th && !r_th->sched.node.is_ready) {
1516 r_th->sched.event_serial++; // a timeout still armed must not wake it again
1517 thread_sched_to_ready_common(sched, r_th, true, false);
1518 }
1519 }
1520 }
1521 thread_sched_unlock(sched, r_th);
1522}
1523
1524static bool
1525ractor_sched_barrier_completed_p(rb_vm_t *vm)
1526{
1527 // The snapshot barrier_running_cnt is taken by the barrier's walk and
1528 // decremented by counted deregistrations; no rescan is needed here.
1529 RUBY_DEBUG_LOG("run:%u wait:%u", vm->ractor.sched.barrier_running_cnt, vm->ractor.sched.barrier_joined_cnt);
1530 VM_ASSERT(vm->ractor.sched.barrier_running_cnt - 1 >= vm->ractor.sched.barrier_joined_cnt);
1531
1532 return (vm->ractor.sched.barrier_running_cnt - vm->ractor.sched.barrier_joined_cnt) == 1;
1533}
1534
1535void
1536rb_ractor_sched_barrier_start(rb_vm_t *vm, rb_ractor_t *cr)
1537{
1538 VM_ASSERT(cr == GET_RACTOR());
1539 VM_ASSERT(vm->ractor.sync.lock_owner == cr); // VM is locked
1540 VM_ASSERT(!vm->ractor.sched.barrier_is_waiting);
1541 VM_ASSERT(vm->ractor.sched.barrier_joined_cnt == 0);
1542 VM_ASSERT(vm->ractor.sched.barrier_ractor == NULL);
1543 VM_ASSERT(vm->ractor.sched.barrier_lock_rec == 0);
1544
1545 RUBY_DEBUG_LOG("start serial:%u", vm->ractor.sched.barrier_serial);
1546
1547 unsigned int lock_rec;
1548
1549 ractor_sched_lock(vm, cr);
1550 {
1551 RUBY_ATOMIC_SET(vm->ractor.sched.barrier_is_waiting, 1);
1552 vm->ractor.sched.barrier_ractor = cr;
1553 vm->ractor.sched.barrier_lock_rec = vm->ractor.sync.lock_rec;
1554
1555 // release VM lock
1556 lock_rec = vm->ractor.sync.lock_rec;
1557 vm->ractor.sync.lock_rec = 0;
1558 vm->ractor.sync.lock_owner = NULL;
1559 rb_native_mutex_unlock(&vm->ractor.sync.lock);
1560
1561 // Interrupt all running threads: running_dnts plus each snt's running_th.
1562 // A switch before this scan is visible to it; one after it sees
1563 // barrier_is_waiting (set above) and waits.
1564 // Interrupt and count every registered runner, stamping each nt so a
1565 // deregistration during this barrier knows it was counted.
1566 rb_thread_t *ith;
1567 unsigned int running_cnt = 0;
1568 uint32_t serial = vm->ractor.sched.barrier_serial;
1569
1570 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
1571 {
1572 struct rb_native_thread *dnt;
1573 ccan_list_for_each(&vm->ractor.sched.ntlist.running_dnts, dnt, running_dnts_node) {
1574 ith = dnt->running_thread;
1575 dnt->barrier_counted_serial = serial;
1576 running_cnt++;
1577 if (ith->ractor != cr) {
1578 RUBY_DEBUG_LOG("barrier request to th:%u", rb_th_serial(ith));
1579 RUBY_VM_SET_VM_BARRIER_INTERRUPT(ith->ec);
1580 }
1581 }
1582
1583 struct rb_native_thread *nt;
1584 ccan_list_for_each(&vm->ractor.sched.ntlist.snts, nt, snts_node) {
1585 rb_native_mutex_lock(&nt->running_th_lock);
1586 {
1587 ith = nt->running_th;
1588 if (ith != NULL) {
1589 nt->barrier_counted_serial = serial;
1590 running_cnt++;
1591 if (ith->ractor != cr) {
1592 RUBY_DEBUG_LOG("barrier request to th:%u", rb_th_serial(ith));
1593 RUBY_VM_SET_VM_BARRIER_INTERRUPT(ith->ec);
1594 }
1595 }
1596 }
1597 rb_native_mutex_unlock(&nt->running_th_lock);
1598 }
1599 }
1600 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
1601
1602 vm->ractor.sched.barrier_running_cnt = running_cnt;
1603
1604 // wait for other ractors
1605 while (!ractor_sched_barrier_completed_p(vm)) {
1606 ractor_sched_set_unlocked(vm, cr);
1607 rb_native_cond_wait(&vm->ractor.sched.barrier_complete_cond, &vm->ractor.sched.lock);
1608 ractor_sched_set_locked(vm, cr);
1609 }
1610
1611 RUBY_DEBUG_LOG("completed seirial:%u", vm->ractor.sched.barrier_serial);
1612
1613 // no other ractors are there
1614 vm->ractor.sched.barrier_serial++;
1615 vm->ractor.sched.barrier_joined_cnt = 0;
1616 rb_native_cond_broadcast(&vm->ractor.sched.barrier_release_cond);
1617
1618 // acquire VM lock
1619 rb_native_mutex_lock(&vm->ractor.sync.lock);
1620 vm->ractor.sync.lock_rec = lock_rec;
1621 vm->ractor.sync.lock_owner = cr;
1622 }
1623
1624 // do not release ractor_sched_lock and there is no newly added (resumed) thread
1625 // thread_sched_setup_running_threads
1626}
1627
1628// called from vm_lock_leave if the vm_lock used for barrierred
1629void
1630rb_ractor_sched_barrier_end(rb_vm_t *vm, rb_ractor_t *cr)
1631{
1632 RUBY_DEBUG_LOG("serial:%u", (unsigned int)vm->ractor.sched.barrier_serial - 1);
1633 VM_ASSERT(vm->ractor.sched.barrier_is_waiting);
1634 VM_ASSERT(vm->ractor.sched.barrier_ractor);
1635 VM_ASSERT(vm->ractor.sched.barrier_lock_rec > 0);
1636
1637 RUBY_ATOMIC_SET(vm->ractor.sched.barrier_is_waiting, 0);
1638 vm->ractor.sched.barrier_ractor = NULL;
1639 vm->ractor.sched.barrier_lock_rec = 0;
1640 ractor_sched_unlock(vm, cr);
1641}
1642
1643static void
1644ractor_sched_barrier_join_signal_locked(rb_vm_t *vm)
1645{
1646 if (ractor_sched_barrier_completed_p(vm)) {
1647 rb_native_cond_signal(&vm->ractor.sched.barrier_complete_cond);
1648 }
1649}
1650
1651static void
1652ractor_sched_barrier_join_wait_locked(rb_vm_t *vm, rb_thread_t *th)
1653{
1654 VM_ASSERT(vm->ractor.sched.barrier_is_waiting);
1655
1656 unsigned int barrier_serial = vm->ractor.sched.barrier_serial;
1657
1658 while (vm->ractor.sched.barrier_serial == barrier_serial) {
1659 RUBY_DEBUG_LOG("sleep serial:%u", barrier_serial);
1660 RB_VM_SAVE_MACHINE_CONTEXT(th);
1661
1662 rb_ractor_t *cr = th->ractor;
1663 ractor_sched_set_unlocked(vm, cr);
1664 rb_native_cond_wait(&vm->ractor.sched.barrier_release_cond, &vm->ractor.sched.lock);
1665 ractor_sched_set_locked(vm, cr);
1666
1667 RUBY_DEBUG_LOG("wakeup serial:%u", barrier_serial);
1668 }
1669}
1670
1671void
1672rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr)
1673{
1674 VM_ASSERT(cr->threads.sched.running != NULL); // running ractor
1675 VM_ASSERT(cr == GET_RACTOR());
1676 VM_ASSERT(vm->ractor.sync.lock_owner == NULL); // VM is locked, but owner == NULL
1677 VM_ASSERT(vm->ractor.sched.barrier_is_waiting); // VM needs barrier sync
1678
1679#if USE_RUBY_DEBUG_LOG || VM_CHECK_MODE > 0
1680 unsigned int barrier_serial = vm->ractor.sched.barrier_serial;
1681#endif
1682
1683 RUBY_DEBUG_LOG("join");
1684
1685 rb_native_mutex_unlock(&vm->ractor.sync.lock);
1686 {
1687 VM_ASSERT(vm->ractor.sched.barrier_is_waiting); // VM needs barrier sync
1688 VM_ASSERT(vm->ractor.sched.barrier_serial == barrier_serial);
1689
1690 ractor_sched_lock(vm, cr);
1691 {
1692 // running_cnt
1693 /* Every joiner is a member of the running set: a dying thread
1694 * leaves the living set before handing over its scheduler slot. */
1695 vm->ractor.sched.barrier_joined_cnt++;
1696 RUBY_DEBUG_LOG("waiting_cnt:%u serial:%u", vm->ractor.sched.barrier_joined_cnt, barrier_serial);
1697
1698 ractor_sched_barrier_join_signal_locked(vm);
1699 ractor_sched_barrier_join_wait_locked(vm, cr->threads.sched.running);
1700 }
1701 ractor_sched_unlock(vm, cr);
1702 }
1703
1704 rb_native_mutex_lock(&vm->ractor.sync.lock);
1705 // VM locked here
1706}
1707
1708// Called when the ractor holding this sched is freed. A drained sched can
1709// still be on timeslice.scheds (pruning is lazy); an unlisted node is
1710// self-linked (fork re-inits them all), making this del a no-op.
1711void
1712rb_thread_sched_destroy(struct rb_thread_sched *sched)
1713{
1714 rb_vm_t *vm = GET_VM();
1715
1716 rb_native_mutex_lock(&vm->ractor.sched.timeslice.lock);
1717 {
1718 ccan_list_del_init(&sched->timeslice_node);
1719 }
1720 rb_native_mutex_unlock(&vm->ractor.sched.timeslice.lock);
1721}
1722
1723#if defined(HAVE_WORKING_FORK)
1724static void rb_internal_thread_event_hooks_rw_lock_atfork(void);
1725
1726static void
1727thread_sched_atfork(struct rb_thread_sched *sched)
1728{
1729 current_fork_gen++;
1730 rb_thread_sched_init(sched, true);
1731 rb_thread_t *th = GET_THREAD();
1732 rb_vm_t *vm = GET_VM();
1733
1734 if (th_has_dedicated_nt(th)) {
1735 vm->ractor.sched.snt_cnt = 0;
1736#if USE_RUBY_DEBUG_LOG
1737 vm->ractor.sched.dnt_cnt = 1;
1738#endif
1739 }
1740 else {
1741 vm->ractor.sched.snt_cnt = 1;
1742#if USE_RUBY_DEBUG_LOG
1743 vm->ractor.sched.dnt_cnt = 0;
1744#endif
1745 }
1746
1747 rb_native_mutex_initialize(&vm->ractor.sched.lock);
1748#if VM_CHECK_MODE > 0
1749 vm->ractor.sched.lock_owner = NULL;
1750 vm->ractor.sched.locked = false;
1751#endif
1752
1753 // rb_native_cond_destroy(&vm->ractor.sched.cond);
1754 rb_native_cond_initialize(&vm->ractor.sched.cond);
1755 rb_native_cond_initialize(&vm->ractor.sched.barrier_complete_cond);
1756 rb_native_cond_initialize(&vm->ractor.sched.barrier_release_cond);
1757
1758 ccan_list_head_init(&vm->ractor.sched.grq);
1759 vm->ractor.sched.grq_cnt = 0; // the list was just emptied; reset the count with it
1760 // A fork during a VM barrier leaves the child with barrier state that can
1761 // never complete (the other ractors are gone); reset it like the rest.
1762 vm->ractor.sched.barrier_is_waiting = 0; // single-threaded child
1763 vm->ractor.sched.barrier_joined_cnt = 0;
1764 vm->ractor.sched.barrier_ractor = NULL;
1765 vm->ractor.sched.barrier_lock_rec = 0;
1766 // Threads that were winding down in the parent do not exist in the child;
1767 // without this reset the child's ruby_vm_destruct would wait for their
1768 // reclaim (which never comes) forever.
1769 vm->ractor.sched.winding_cnt = 0;
1770 rb_native_mutex_initialize(&vm->ractor.sched.ntlist.lock);
1771 ccan_list_head_init(&vm->ractor.sched.ntlist.running_dnts);
1772 ccan_list_head_init(&vm->ractor.sched.ntlist.snts); // those nts are gone
1773 rb_native_mutex_initialize(&vm->ractor.sched.timeslice.lock);
1774 ccan_list_head_init(&vm->ractor.sched.timeslice.scheds);
1775 rb_native_mutex_initialize(&th->nt->running_th_lock); // a scan could hold it at fork
1776 th->nt->running_th = NULL; // th re-records itself below
1777 th->nt->retiring = false; // the pool it had no room in is gone
1778 native_main_thread_atfork(); // this pthread is the process's main one now
1779 // Fork can copy nodes linked (or torn mid-link); re-init every sched's
1780 // node so rb_thread_sched_destroy's del_init stays a no-op for them.
1781 rb_ractor_t *r;
1782 ccan_list_for_each(&vm->ractor.set, r, vmlr_node) {
1783 ccan_list_node_init(&r->threads.sched.timeslice_node);
1784 }
1785 ccan_list_for_each(&vm->ractor.terminated_set, r, vmlr_node) {
1786 ccan_list_node_init(&r->threads.sched.timeslice_node);
1787 }
1788 // th re-records itself below; the parent's record did not survive the lists
1789 if (th->nt && th->nt->dedicated == 0) {
1790 // surviving on an snt: put that nt back on the (just emptied) snts
1791 // list, or the scans could not see this thread's record
1792 ccan_list_add(&vm->ractor.sched.ntlist.snts, &th->nt->snts_node);
1793 }
1794
1795#if USE_MN_THREADS
1796 nt_machine_stack_atfork();
1797#endif
1798 rb_internal_thread_event_hooks_rw_lock_atfork();
1799
1800 VM_ASSERT(sched->is_running);
1801
1802 if (sched->running != th) {
1803 thread_sched_to_running(sched, th);
1804 }
1805 else {
1806 thread_sched_setup_running_threads(sched, th->ractor, vm, th, NULL);
1807 }
1808
1809 if (th->nt) {
1810 // no known pthread gives the child a new pthread_t, but just in case
1811 th->nt->thread_id = pthread_self();
1812#ifdef RB_THREAD_T_HAS_NATIVE_ID
1813 th->nt->tid = get_native_thread_id();
1814#endif
1815 }
1816}
1817
1818#endif
1819
1820extern int ruby_mn_threads_enabled;
1821
1822void
1823ruby_mn_threads_params(void)
1824{
1825 rb_vm_t *vm = GET_VM();
1826 rb_ractor_t *main_ractor = GET_RACTOR();
1827
1828 // RUBY_MN_THREADS: -1 = nothing is M:N, 0 = the default, 1 = the main
1829 // Ractor's threads too, 2 = the main thread as well (see
1830 // thread_sched_main_to_shared). The main Ractor's sched already exists
1831 // here, so it is set rather than defaulted.
1832 const char *mn_threads_cstr = getenv("RUBY_MN_THREADS");
1833 int mn_threads = (USE_MN_THREADS && mn_threads_cstr) ? atoi(mn_threads_cstr) : 0;
1834
1835 mn_threads_mode = mn_threads;
1836 if (mn_threads > 0) {
1837 ruby_mn_threads_enabled = mn_threads;
1838 }
1839 // =2 publishes it from thread_sched_main_to_shared, with the main nt's
1840 // pool entry, so that the timer thread cannot mint an snt in between
1841 main_ractor->threads.sched.enable_mn_threads = mn_threads == 1;
1842
1843 const char *max_cpu_cstr = getenv("RUBY_MAX_CPU");
1844 int max_cpu = native_thread_default_max_cpu();
1845
1846 if (USE_MN_THREADS && max_cpu_cstr) {
1847 int given_max_cpu = atoi(max_cpu_cstr);
1848 if (given_max_cpu > 0) {
1849 max_cpu = given_max_cpu;
1850 }
1851 }
1852
1853 vm->ractor.sched.max_cpu = max_cpu;
1854
1855#if USE_MN_THREADS
1856 if (mn_threads >= 2) {
1857 thread_sched_main_to_shared(GET_THREAD());
1858 }
1859#endif
1860}
1861
1862static void
1863native_thread_dedicated_inc(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_thread *nt)
1864{
1865 RUBY_DEBUG_LOG("nt:%d %d->%d", nt->serial, nt->dedicated, nt->dedicated + 1);
1866
1867 // A retiring nt left snt_cnt when it last came back (see
1868 // native_thread_dedicated_dec); taking it out again would leave the pool
1869 // looking a thread short, and the timer thread would make another.
1870 if (nt->dedicated == 0 && !nt->retiring) {
1871 // Lock-free; pairs with ractor_sched_enq (enq: grq_cnt up then read
1872 // snt_cnt / here: snt_cnt down then read grq_cnt) against lost wakeups.
1873 if (RUBY_ATOMIC_FETCH_SUB(vm->ractor.sched.snt_cnt, 1) == 1) {
1874 // the last snt went dedicated; pending entries need the timer thread
1875 ractor_sched_lock(vm, cr);
1876 {
1877 if (vm->ractor.sched.grq_cnt > 0) {
1878 timer_thread_wakeup_locked(vm);
1879 }
1880 }
1881 ractor_sched_unlock(vm, cr);
1882 }
1883 }
1884#if USE_RUBY_DEBUG_LOG
1885 if (nt->dedicated == 0) vm->ractor.sched.dnt_cnt++; // _dec takes every nt out
1886#endif
1887
1888 nt->dedicated++;
1889}
1890
1891static void
1892native_thread_dedicated_dec(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_thread *nt)
1893{
1894 RUBY_DEBUG_LOG("nt:%d %d->%d", nt->serial, nt->dedicated, nt->dedicated - 1);
1895 VM_ASSERT(nt->dedicated > 0);
1896 nt->dedicated--;
1897
1898 if (nt->dedicated == 0) {
1899 // Rejoin under the max_cpu cap; with no room this nt retires and
1900 // belongs to neither count until it ends. The process's main nt
1901 // cannot end (its loop runs on a stack it would have to free), so
1902 // it always rejoins.
1903 const bool can_retire = native_thread_self_can_retire_p();
1904 while (1) {
1905 rb_atomic_t snt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
1906 if (!can_retire || snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
1907 if (RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt, snt + 1) == snt) {
1908 nt->retiring = false; // room now, after having none before
1909 break;
1910 }
1911 }
1912 else {
1913 nt->retiring = true;
1914 break;
1915 }
1916 }
1917#if USE_RUBY_DEBUG_LOG
1918 vm->ractor.sched.dnt_cnt--;
1919#endif
1920 }
1921}
1922
1923static void
1924native_thread_assign(struct rb_native_thread *nt, rb_thread_t *th)
1925{
1926#if USE_RUBY_DEBUG_LOG
1927 if (nt) {
1928 if (th->nt) {
1929 RUBY_DEBUG_LOG("th:%d nt:%d->%d", (int)th->serial, (int)th->nt->serial, (int)nt->serial);
1930 }
1931 else {
1932 RUBY_DEBUG_LOG("th:%d nt:NULL->%d", (int)th->serial, (int)nt->serial);
1933 }
1934 }
1935 else {
1936 if (th->nt) {
1937 RUBY_DEBUG_LOG("th:%d nt:%d->NULL", (int)th->serial, (int)th->nt->serial);
1938 }
1939 else {
1940 RUBY_DEBUG_LOG("th:%d nt:NULL->NULL", (int)th->serial);
1941 }
1942 }
1943#endif
1944
1945 th->nt = nt;
1946}
1947
1948static int
1949native_thread_create_dedicated(rb_thread_t *th)
1950{
1951 th->nt = native_thread_alloc();
1952 th->nt->vm = th->vm;
1953 th->nt->running_thread = th;
1954 th->nt->dedicated = 1;
1955
1956 // vm stack
1957 size_t vm_stack_word_size = th->vm->default_params.thread_vm_stack_size / sizeof(VALUE);
1958 void *vm_stack = ruby_xmalloc(vm_stack_word_size * sizeof(VALUE));
1959 th->sched.malloc_stack = true;
1960 rb_ec_initialize_vm_stack(th->ec, vm_stack, vm_stack_word_size);
1961 th->sched.context_stack = vm_stack;
1962 th->sched.context_stack_size = vm_stack_word_size;
1963
1964 int err = native_thread_create0(th->nt);
1965 if (!err) {
1966 // setup
1967 thread_sched_to_ready(TH_SCHED(th), th);
1968 }
1969 return err;
1970}
1971
1972static void
1973call_thread_start_func_2(rb_thread_t *th)
1974{
1975 /* Capture the address of a local in this stack frame to mark the beginning of the
1976 machine stack for this thread. This is required even if we can tell the real
1977 stack beginning from the pthread API in native_thread_init_stack, because
1978 glibc stores some of its own data on the stack before calling into user code
1979 on a new thread, and replacing that data on fiber-switch would break it (see
1980 bug #13887) */
1981 VALUE stack_start = 0;
1982 VALUE *stack_start_addr = asan_get_real_stack_addr(&stack_start);
1983
1984 native_thread_init_stack(th, stack_start_addr);
1985 thread_start_func_2(th, th->ec->machine.stack_start);
1986}
1987
1988static void *
1989nt_start(void *ptr)
1990{
1991 struct rb_native_thread *nt = (struct rb_native_thread *)ptr;
1992 rb_vm_t *vm = nt->vm;
1993
1994 native_thread_setup_on_thread(nt);
1995
1996#if USE_RUBY_DEBUG_LOG && defined(RUBY_NT_SERIAL)
1997 ruby_nt_serial = nt->serial;
1998#endif
1999
2000 RUBY_DEBUG_LOG("nt:%u", nt->serial);
2001
2002 if (nt->dedicated) {
2003 // wait running turn
2004 rb_thread_t *th = nt->running_thread;
2005 struct rb_thread_sched *sched = TH_SCHED(th);
2006
2007 RUBY_DEBUG_LOG("on dedicated th:%u", rb_th_serial(th));
2008 ruby_thread_set_native(th);
2009
2010 thread_sched_lock(sched, th);
2011 {
2012 if (sched->running == th) {
2013 thread_sched_add_running_thread(sched, th);
2014 }
2015 thread_sched_wait_running_turn(sched, th, false, NULL);
2016 }
2017 thread_sched_unlock(sched, th);
2018
2019 // start threads
2020 call_thread_start_func_2(th);
2021 // TODO: allow to change to the SNT
2022 }
2023 else {
2024 coroutine_initialize_main(nt->nt_context);
2025 nt_snts_join(vm, nt);
2026
2027 bool retired = nt_shared_loop(nt);
2028 nt_snts_leave(vm, nt);
2029
2030 if (retired) {
2031 // The counts dropped this nt already; nothing can reference it now.
2032 RUBY_DEBUG_LOG("retired nt:%u", nt->serial);
2033 native_thread_destroy_self(nt);
2034 }
2035 }
2036
2037 return NULL;
2038}
2039
2040// join the snt list that the barrier/timeslice scans walk
2041static void
2042nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt)
2043{
2044 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
2045 {
2046 ccan_list_add(&vm->ractor.sched.ntlist.snts, &nt->snts_node);
2047 }
2048 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
2049}
2050
2051// Leaving the shared loop: every path back here deregistered first
2052// (park and death both precede the transfer), so only the snts entry
2053// is left to remove.
2054static void
2055nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt)
2056{
2057 VM_ASSERT(nt->running_th == NULL);
2058 rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
2059 {
2060 ccan_list_del_init(&nt->snts_node);
2061 }
2062 rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
2063}
2064
2065// The shared nt's scheduling loop: serve Ractors from the grq until this nt
2066// retires (returns true) or goes dedicated while running (returns false).
2067static bool
2068nt_shared_loop(struct rb_native_thread *nt)
2069{
2070 rb_vm_t *vm = nt->vm;
2071
2072 while (1) {
2073 RUBY_DEBUG_LOG("check next");
2074 if (nt->retiring) { // came back with no room in the shared pool
2075 return true;
2076 }
2077
2078 // asked every time: a fork leaves this loop's native thread as the
2079 // child's main one, which may not retire
2080 rb_ractor_t *r = ractor_sched_deq(vm, NULL, native_thread_self_can_retire_p());
2081
2082 if (r) {
2083 struct rb_thread_sched *sched = &r->threads.sched;
2084
2085 bool locked = true;
2086
2087 thread_sched_lock(sched, NULL);
2088 {
2089 rb_thread_t *next_th = sched->running;
2090
2091 if (next_th && next_th->nt == NULL) {
2092 RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
2093#if USE_MN_THREADS
2094 thread_sched_switch0(nt->nt_context, next_th, nt, false);
2095
2096 // If a coroutine terminated during the transfer, co_start
2097 // recorded it in nt->dead_co (switch0's return value is
2098 // backend-dependent, unusable; see thread_pthread.h).
2099 struct coroutine_context *dead_co = nt->dead_co;
2100 nt->dead_co = NULL;
2101 if (thread_sched_reclaim(dead_co)) {
2102 // it already released the sched lock before its
2103 // transfer (its Ractor may be gone): leave sched be.
2104 locked = false;
2105 }
2106#else
2107 thread_sched_switch0(nt->nt_context, next_th, nt, false);
2108#endif
2109 }
2110 else {
2111 RUBY_DEBUG_LOG("no schedulable threads -- next_th:%p", next_th);
2112 }
2113 }
2114 if (locked) {
2115 thread_sched_unlock(sched, NULL);
2116 }
2117 }
2118 else {
2119 // ractor_sched_deq retired this nt.
2120 return true;
2121 }
2122
2123 if (nt->dedicated) {
2124 // SNT becomes DNT while running
2125 return false;
2126 }
2127 }
2128}
2129
2130#if USE_MN_THREADS
2131// The scheduling loop of the process's main nt, entered when RUBY_MN_THREADS=2
2132// turned it shared: the process stack belongs to the main thread's context,
2133// so this loop runs on a coroutine of its own (thread_sched_main_to_shared).
2134static COROUTINE
2135nt_loop_co(struct coroutine_context *from, struct coroutine_context *self)
2136{
2137#ifdef RUBY_ASAN_ENABLED
2138 __sanitizer_finish_switch_fiber(self->fake_stack,
2139 (const void**)&from->stack_base, &from->stack_size);
2140#endif
2141 struct rb_native_thread *nt = (struct rb_native_thread *)self->argument;
2142
2143 // The first entry is a transfer that in nt_shared_loop would return from
2144 // thread_sched_switch0: a parked thread left its sched lock held for the
2145 // loop to release, or a terminated one (dead_co) released it itself.
2146 struct coroutine_context *dead_co = nt->dead_co;
2147 nt->dead_co = NULL;
2148 if (!thread_sched_reclaim(dead_co)) {
2149 rb_thread_t *parked_th = (rb_thread_t *)from->argument;
2150 thread_sched_unlock(TH_SCHED(parked_th), NULL);
2151 }
2152
2153 // as after a switch0 return: the thread may have pinned this nt
2154 // (rb_thread_lock_native_thread) before it ended
2155 if (!nt->dedicated) {
2156 if (nt_shared_loop(nt)) rb_bug("main nt retired"); // native_thread_self_can_retire_p() is false here
2157 }
2158 nt_snts_leave(nt->vm, nt);
2159
2160 // Went dedicated while running (rb_thread_lock_native_thread) and that
2161 // thread ended. Nothing can resume this context; sleep out the process.
2162 while (1) {
2163 pause();
2164 }
2165}
2166
2167// RUBY_MN_THREADS=2: make the running main thread an M:N thread in place.
2168// Its context becomes the process stack (as nt_start's own stack is an snt's
2169// context) and its nt joins the shared pool with a fresh stack for its loop.
2170// Nothing changes for the thread until it first blocks: that transfer
2171// starts nt_loop_co on this nt, and any snt may resume the thread later.
2172static void
2173thread_sched_main_to_shared(rb_thread_t *th)
2174{
2175 rb_vm_t *vm = th->vm;
2176 struct rb_native_thread *nt = th->nt;
2177 struct rb_thread_sched *sched = TH_SCHED(th);
2178
2179 VM_ASSERT(th == vm->ractor.main_thread);
2180 VM_ASSERT(nt->dedicated == 1 && th->has_dedicated_nt);
2181 VM_ASSERT(sched->running == th);
2182
2183 // the loop's stack (the vm stack half of the pool slot goes unused)
2184 void *vm_stack, *machine_stack;
2185 int err = nt_alloc_stack(vm, &vm_stack, &machine_stack);
2186 if (err) {
2187 rb_warn("RUBY_MN_THREADS=2: cannot allocate the main nt's stack (%s); the main thread stays dedicated", strerror(err));
2188 th->ractor->threads.sched.enable_mn_threads = true; // as =1
2189 return;
2190 }
2191 size_t machine_stack_size = vm->default_params.thread_machine_stack_size - sizeof(struct nt_machine_stack_footer);
2192 // the main nt is ZALLOC'd by Init_BareVM, not native_thread_alloc: no context yet
2193 nt->nt_context = ruby_xmalloc(sizeof(struct coroutine_context));
2194 coroutine_initialize(nt->nt_context, nt_loop_co, machine_stack, machine_stack_size);
2195 nt->nt_context->argument = nt;
2196
2197 // the thread's context is the process stack it already runs on
2198 struct rb_thread_context *tctx = ruby_xmalloc(sizeof(struct rb_thread_context));
2199 tctx->stack = NULL; // not a pool stack: never freed
2200 tctx->dead = false;
2201 tctx->nt = NULL;
2202 coroutine_initialize_main(&tctx->co);
2203 tctx->co.argument = th;
2204 th->sched.context = &tctx->co;
2205
2206 thread_sched_lock(sched, th);
2207 {
2208 // re-record the running thread as an snt's (running_dnts -> nt->running_th)
2209 thread_sched_del_running_thread(sched, th);
2210 nt->dedicated = 0;
2211 th->has_dedicated_nt = 0;
2212 nt_snts_join(vm, nt);
2213 // the pool's first snt; under the lock native_thread_check_and_create_shared
2214 // takes, so that it never mints one for the main Ractor alongside
2215 ractor_sched_lock(vm, th->ractor);
2216 {
2217 // The pool is still empty: the timer thread has been up since
2218 // Init_Thread and would mint one here, but its timeout branch is
2219 // the only path there and it sleeps untimed with nothing waiting.
2220 VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0);
2221 RUBY_ATOMIC_INC(vm->ractor.sched.snt_cnt);
2222 th->ractor->threads.sched.enable_mn_threads = true;
2223 }
2224 ractor_sched_unlock(vm, th->ractor);
2225#if USE_RUBY_DEBUG_LOG
2226 vm->ractor.sched.dnt_cnt--;
2227#endif
2228 thread_sched_add_running_thread(sched, th);
2229 }
2230 thread_sched_unlock(sched, th);
2231
2232 RUBY_DEBUG_LOG("main th:%u on nt:%d is now shared", rb_th_serial(th), nt->serial);
2233}
2234#endif
2235
2236static int native_thread_create_shared(rb_thread_t *th);
2237
2238#if USE_MN_THREADS
2239static void nt_free_stack(void *mstack);
2240
2241
2242// Reclaim the context a coroutine thread recorded in nt->dead_co before its
2243// final transfer (co_start's epilogue). Our running here proves that transfer's
2244// register save into the block completed. Returns true when a thread did
2245// terminate -- it RELEASED the sched lock before transferring; NULL/false means
2246// a live yield, where the loop still owns the lock.
2247static bool
2248thread_sched_reclaim(struct coroutine_context *dead_co)
2249{
2250 struct rb_thread_context *tctx = (struct rb_thread_context *)dead_co;
2251
2252 if (tctx != NULL && tctx->dead) {
2253 nt_free_stack(tctx->stack);
2254 SIZED_FREE(tctx);
2255 // pairs with the increment at the top of coroutine_thread_terminated:
2256 // a waiting VM destruct may proceed once this reclaim is done
2257 VM_ASSERT(RUBY_ATOMIC_LOAD(GET_VM()->ractor.sched.winding_cnt) > 0);
2258 RUBY_ATOMIC_DEC(GET_VM()->ractor.sched.winding_cnt);
2259 return true;
2260 }
2261 return false;
2262}
2263#endif
2264
2265void
2266rb_thread_wake_fence(rb_thread_t *th)
2267{
2268 timer_thread_wake_fence(th);
2269}
2270
2271void
2272rb_threadptr_sched_free(rb_thread_t *th)
2273{
2274 timer_thread_wake_fence(th);
2275#if USE_MN_THREADS
2276 // A thread with a coroutine context runs on a native thread it shares and
2277 // does not own: one made for the shared pool, or the main thread made
2278 // shared. nt->dedicated does not say so, rb_thread_lock_native_thread()
2279 // raising it on a shared native thread that stays in the pool.
2280 const bool owns_nt = th->sched.context == NULL;
2281
2282 if (th->sched.context != NULL) {
2283 // a coroutine thread that never reached its epilogue (never started),
2284 // or the main thread made shared (its stack is the process stack);
2285 // a terminated one is reclaimed by whoever resumed from its final
2286 // transfer (thread_sched_reclaim), and cleared this pointer.
2287 struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context;
2288 nt_free_stack(tctx->stack);
2289 SIZED_FREE(tctx);
2290 th->sched.context = NULL;
2291 // TODO: how to free nt and nt->altstack?
2292 }
2293 if (th->sched.malloc_stack) {
2294 SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
2295 if (th->nt && owns_nt) {
2296 native_thread_destroy(th->nt);
2297 }
2298 }
2299#else
2300 SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
2301 native_thread_destroy(th->nt);
2302#endif
2303
2304 th->nt = NULL;
2305}
2306
2307
2308static int
2309native_thread_create(rb_thread_t *th)
2310{
2311 VM_ASSERT(th->nt == 0);
2312 RUBY_DEBUG_LOG("th:%d has_dnt:%d", th->serial, th->has_dedicated_nt);
2313 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_STARTED, th);
2314
2315 if (!th->ractor->threads.sched.enable_mn_threads) {
2316 th->has_dedicated_nt = 1;
2317 }
2318
2319 if (th->has_dedicated_nt) {
2320 return native_thread_create_dedicated(th);
2321 }
2322 else {
2323 return native_thread_create_shared(th);
2324 }
2325}
2326
2327#ifdef USE_UBF_LIST
2328static CCAN_LIST_HEAD(ubf_list_head);
2329#ifdef RB_NATIVETHREAD_LOCK_INIT
2330static rb_nativethread_lock_t ubf_list_lock = RB_NATIVETHREAD_LOCK_INIT;
2331#else
2332// no static initializer on this platform; thread_sched_init_vm() does it
2333static rb_nativethread_lock_t ubf_list_lock;
2334#endif
2335
2336static void
2337ubf_list_atfork(void)
2338{
2339 ccan_list_head_init(&ubf_list_head);
2340 rb_native_mutex_initialize(&ubf_list_lock);
2341}
2342
2344static bool
2345ubf_list_contain_p(rb_thread_t *th)
2346{
2347 rb_thread_t *list_th;
2348 ccan_list_for_each(&ubf_list_head, list_th, sched.node.ubf) {
2349 if (list_th == th) return true;
2350 }
2351 return false;
2352}
2353
2354/* The thread 'th' is registered to be trying unblock. */
2355static void
2356register_ubf_list(rb_thread_t *th)
2357{
2358 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
2359 struct ccan_list_node *node = &th->sched.node.ubf;
2360
2361 VM_ASSERT(th->unblock.func != NULL);
2362
2363 rb_native_mutex_lock(&ubf_list_lock);
2364 {
2365 // check not connected yet
2366 if (ccan_list_empty((struct ccan_list_head*)node)) {
2367 VM_ASSERT(!ubf_list_contain_p(th));
2368 ccan_list_add(&ubf_list_head, node);
2369 }
2370 }
2371 rb_native_mutex_unlock(&ubf_list_lock);
2372
2373 timer_thread_wakeup();
2374}
2375
2376/* The thread 'th' is unblocked. It no longer need to be registered. */
2377static void
2378unregister_ubf_list(rb_thread_t *th)
2379{
2380 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
2381 struct ccan_list_node *node = &th->sched.node.ubf;
2382
2383 /* we can't allow re-entry into ubf_list_head */
2384 VM_ASSERT(th->unblock.func == NULL);
2385
2386 if (!ccan_list_empty((struct ccan_list_head*)node)) {
2387 rb_native_mutex_lock(&ubf_list_lock);
2388 {
2389 VM_ASSERT(ubf_list_contain_p(th));
2390 ccan_list_del_init(node);
2391 }
2392 rb_native_mutex_unlock(&ubf_list_lock);
2393 }
2394}
2395
2396/*
2397 * Poke the target thread so that it returns from a blocking syscall.
2398 * How that is done is up to the platform (native_thread_interrupt).
2399 */
2400static void
2401ubf_wakeup_thread(rb_thread_t *th)
2402{
2403 RUBY_DEBUG_LOG("th:%u thread_id:%p", rb_th_serial(th), (void *)th->nt->thread_id);
2404
2405 native_thread_interrupt(th);
2406}
2407
2408static void
2409ubf_select(void *ptr)
2410{
2411 rb_thread_t *th = (rb_thread_t *)ptr;
2412 RUBY_DEBUG_LOG("wakeup th:%u", rb_th_serial(th));
2413 ubf_wakeup_thread(th);
2414 register_ubf_list(th);
2415}
2416
2417static bool
2418ubf_threads_empty(void)
2419{
2420 return ccan_list_empty(&ubf_list_head) != 0;
2421}
2422
2423static void
2424ubf_wakeup_all_threads(void)
2425{
2426 rb_thread_t *th;
2427 rb_native_mutex_lock(&ubf_list_lock);
2428 {
2429 ccan_list_for_each(&ubf_list_head, th, sched.node.ubf) {
2430 ubf_wakeup_thread(th);
2431 }
2432 }
2433 rb_native_mutex_unlock(&ubf_list_lock);
2434}
2435
2436#else /* USE_UBF_LIST */
2437#define register_ubf_list(th) (void)(th)
2438#define unregister_ubf_list(th) (void)(th)
2439#define ubf_select 0
2440static void ubf_wakeup_all_threads(void) { return; }
2441static bool ubf_threads_empty(void) { return true; }
2442#define ubf_list_atfork() do {} while (0)
2443#endif /* USE_UBF_LIST */
2444
2445static int
2446timer_thread_set_timeout(rb_vm_t *vm)
2447{
2448#if 0
2449 return 10; // ms
2450#else
2451 int timeout = -1;
2452
2453 ractor_sched_lock(vm, NULL);
2454 {
2455 if ( timeslice_scan(vm, false) // (1-1) Provide time slice for active NTs
2456 || !ubf_threads_empty() // (1-3) Periodic UBF
2457 || vm->ractor.sched.grq_cnt > 0 // (1-4) Lazy GRQ deq start
2458 ) {
2459
2460 RUBY_DEBUG_LOG("ubf:%d grq:%d",
2461 !ubf_threads_empty(),
2462 (vm->ractor.sched.grq_cnt > 0));
2463
2464 timeout = 10; // ms
2465 vm->ractor.sched.timeslice_wait_inf = false;
2466 }
2467 else {
2468 vm->ractor.sched.timeslice_wait_inf = true;
2469 }
2470 }
2471 ractor_sched_unlock(vm, NULL);
2472
2473 timeout = timer_wheel_timeout(timeout);
2474
2475 RUBY_DEBUG_LOG("timeout:%d inf:%d", timeout, (int)vm->ractor.sched.timeslice_wait_inf);
2476
2477 // fprintf(stderr, "timeout:%d\n", timeout);
2478 return timeout;
2479#endif
2480}
2481
2482static void
2483timer_thread_check_signal(rb_vm_t *vm)
2484{
2485 // ruby_sigchld_handler(vm); TODO
2486
2487 int signum = rb_signal_buff_size();
2488 if (UNLIKELY(signum > 0) && vm->ractor.main_thread) {
2489 RUBY_DEBUG_LOG("signum:%d", signum);
2490 threadptr_trap_interrupt(vm->ractor.main_thread);
2491 }
2492}
2493
2494// Tick (with `interrupt`) each listed sched's running thread and prune scheds
2495// whose readyq drained; returns whether any sched still needs ticks.
2496static bool
2497timeslice_scan(rb_vm_t *vm, bool interrupt)
2498{
2499 bool found = false;
2500 struct rb_thread_sched *sched, *next;
2501
2502 rb_native_mutex_lock(&vm->ractor.sched.timeslice.lock);
2503 {
2504 ccan_list_for_each_safe(&vm->ractor.sched.timeslice.scheds, sched, next, timeslice_node) {
2505 // trylock: timeslice_sched_link nests sched.lock -> timeslice.lock,
2506 // this scan holds the locks the other way around
2507 if (rb_native_mutex_trylock(&sched->lock_) == 0) {
2508 if (ccan_list_empty(&sched->readyq)) {
2509 ccan_list_del_init(&sched->timeslice_node); // a later enq relinks it
2510 }
2511 else if (sched->is_running) {
2512 VM_ASSERT(sched->running != NULL);
2513 found = true;
2514 if (interrupt) {
2515 RUBY_DEBUG_LOG("timeslice th:%u", rb_th_serial(sched->running));
2516 RUBY_VM_SET_TIMER_INTERRUPT(sched->running->ec);
2517 }
2518 }
2519 // else: waiters behind a blocked runner need no ticks; the
2520 // add path wakes the timer when the sched runs again
2521 rb_native_mutex_unlock(&sched->lock_);
2522 }
2523 else {
2524 found = true; // busy switching; tick it on the next round
2525 }
2526 }
2527 }
2528 rb_native_mutex_unlock(&vm->ractor.sched.timeslice.lock);
2529
2530 return found;
2531}
2532
2533static void
2534timer_thread_check_timeslice(rb_vm_t *vm)
2535{
2536 // TODO: check time
2537 timeslice_scan(vm, true);
2538}
2539
2540static void *
2541timer_thread_func(void *ptr)
2542{
2543 rb_vm_t *vm = (rb_vm_t *)ptr;
2544#if defined(RUBY_NT_SERIAL)
2545 ruby_nt_serial = (rb_atomic_t)-1;
2546#endif
2547
2548 RUBY_DEBUG_LOG("started%s", "");
2549
2550 while (RUBY_ATOMIC_LOAD(system_working)) {
2551 timer_thread_check_signal(vm);
2552 timer_thread_check_timeout(vm);
2553 ubf_wakeup_all_threads();
2554
2555 RUBY_DEBUG_LOG("system_working:%d", RUBY_ATOMIC_LOAD(system_working));
2556 timer_thread_polling(vm);
2557 }
2558
2559 RUBY_DEBUG_LOG("terminated");
2560 return NULL;
2561}
2562
2563static void
2564timer_thread_wakeup_locked(rb_vm_t *vm)
2565{
2566 // should be locked before.
2567 ASSERT_ractor_sched_locked(vm, NULL);
2568
2569 if (TIMER_THREAD_CREATED_P()) {
2570 if (vm->ractor.sched.timeslice_wait_inf) {
2571 RUBY_DEBUG_LOG("wakeup%s", "");
2572 timer_thread_wakeup_force();
2573 }
2574 else {
2575 RUBY_DEBUG_LOG("will be wakeup...");
2576 }
2577 }
2578}
2579
2580static void
2581timer_thread_wakeup(void)
2582{
2583 rb_vm_t *vm = GET_VM();
2584
2585 ractor_sched_lock(vm, NULL);
2586 {
2587 timer_thread_wakeup_locked(vm);
2588 }
2589 ractor_sched_unlock(vm, NULL);
2590}
2591
2592static void
2593native_sleep(rb_thread_t *th, rb_hrtime_t *rel)
2594{
2595 struct rb_thread_sched *sched = TH_SCHED(th);
2596
2597 RUBY_DEBUG_LOG("rel:%d", rel ? (int)*rel : 0);
2598
2599 if (rel && !th_has_dedicated_nt(th)) {
2600 // an M:N thread has no condvar of its own: the timer thread wakes it
2601 thread_sched_wait_events(sched, th, -1, thread_sched_waiting_timeout, rel);
2602 }
2603 else if (rel) {
2604 /* Solaris cond_timedwait() returns EINVAL if an argument is greater than
2605 * current_time + 100,000,000. So cut up to 100,000,000. This is
2606 * considered as a kind of spurious wakeup. The caller to native_sleep
2607 * should care about spurious wakeup.
2608 *
2609 * See also [Bug #1341] [ruby-core:29702]
2610 * http://download.oracle.com/docs/cd/E19683-01/816-0216/6m6ngupgv/index.html
2611 */
2612 const rb_hrtime_t max = (rb_hrtime_t)100000000 * RB_HRTIME_PER_SEC;
2613 if (*rel > max) *rel = max;
2614
2615 rb_hrtime_t end = rb_hrtime_add(rb_hrtime_now(), *rel);
2616 thread_sched_to_waiting_until_wakeup(sched, th, &end);
2617 }
2618 else {
2619 thread_sched_to_waiting_until_wakeup(sched, th, NULL);
2620 }
2621
2622 RUBY_DEBUG_LOG("wakeup");
2623}
2624
2625
2626// return true if the current thread acquires DNT.
2627// return false if the current thread already acquires DNT.
2628bool
2630{
2631 rb_thread_t *th = GET_THREAD();
2632 bool is_snt = th->nt->dedicated == 0;
2633 native_thread_dedicated_inc(th->vm, th->ractor, th->nt);
2634
2635 return is_snt;
2636}
2637
2638// rb_ractor_terminate_all() waits for the Ractors it interrupted on a condvar
2639// of its own, holding the VM lock, which it drops for the wait and takes back
2640// after. An M:N thread would hold the shared native thread it runs on for the
2641// whole wait as well, leaving those Ractors with nothing to run on, so give
2642// that native thread back the way a blocking region does.
2643void
2644rb_ractor_sched_wait_terminate(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
2645{
2646 ASSERT_vm_locking();
2647
2648 rb_thread_t *th = GET_THREAD();
2649 unsigned int lock_rec = vm->ractor.sync.lock_rec;
2650 rb_ractor_t *lock_owner = vm->ractor.sync.lock_owner;
2651
2652 vm->ractor.sync.lock_rec = 0;
2653 vm->ractor.sync.lock_owner = NULL;
2654
2655 native_thread_dedicated_inc(vm, th->ractor, th->nt);
2656 rb_native_cond_timedwait(cond, &vm->ractor.sync.lock, msec);
2657 native_thread_dedicated_dec(vm, th->ractor, th->nt);
2658
2659 vm->ractor.sync.lock_rec = lock_rec;
2660 vm->ractor.sync.lock_owner = lock_owner;
2661}
2662
2663void
2664rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)
2665{
2666 th->sched.malloc_stack = true;
2667 th->sched.context_stack = stack;
2668 th->sched.context_stack_size = stack_size;
2669}
2670
2671// VM wide scheduler state, shared by every platform. Called from
2672// Init_native_thread() before the main thread is recorded.
2673static void
2674thread_sched_init_vm(rb_vm_t *vm)
2675{
2676 rb_native_mutex_initialize(&vm->ractor.sched.lock);
2677 rb_native_cond_initialize(&vm->ractor.sched.cond);
2678 rb_native_cond_initialize(&vm->ractor.sched.barrier_complete_cond);
2679 rb_native_cond_initialize(&vm->ractor.sched.barrier_release_cond);
2680
2681 ccan_list_head_init(&vm->ractor.sched.grq);
2682 rb_native_mutex_initialize(&vm->ractor.sched.ntlist.lock);
2683 ccan_list_head_init(&vm->ractor.sched.ntlist.running_dnts);
2684 ccan_list_head_init(&vm->ractor.sched.ntlist.snts);
2685 rb_native_mutex_initialize(&vm->ractor.sched.timeslice.lock);
2686 ccan_list_head_init(&vm->ractor.sched.timeslice.scheds);
2687
2688#ifndef RB_NATIVETHREAD_LOCK_INIT
2689 // ubf_list_lock could not be initialized statically
2690 ubf_list_atfork();
2691#endif
2692}
#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:464
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