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