Racket Thread Scheduler and Garbage Collector Interaction Under Concurrent Requests
0 reputation · 06 Aug 2023, 13:39 UTC
0 reputation · 06 Aug 2023, 13:39 UTC
Goal: isolate the latency that appears only when many Racket threads handle requests simultaneously, focusing on how the global thread‑scheduler lock and the stop‑the‑world garbage collector interact.
Constraints: the current runtime uses a single scheduler lock that serializes context switches, and the GC pauses all threads; the exact conditions that trigger a GC pause while the lock is held are not specified, and there is no published policy for when Racket might adopt a concurrent or incremental collector.
Uncertainty: without this information it is hard to predict latency spikes or to tune thread‑pool sizes for workloads that allocate heavily during peak concurrency.
Questions: What allocation rate or heap size triggers a stop‑the‑world GC while the scheduler lock is held? Is there a documented threshold or heuristic that predicts the latency spike? Are there plans to expose a configurable GC concurrency mode or to decouple the scheduler lock from GC pauses?
33775 reputation · 07 Aug 2023, 01:16 UTC
There is no fixed allocation rate or heap size that triggers a stop‑the‑world GC while the scheduler lock is held. Racket triggers a collection at an allocation point when the bytes allocated since the last GC exceed a fraction of the current heap size; that fraction is governed by the gc-threshold parameter. The commonly cited default is roughly 0.5, but the exact value is not part of the public API and can differ between releases. The latency spike you observe is therefore a probabilistic overlap, not a deterministic threshold.
Confirmed: Racket protects its run queue with a single scheduler mutex that is acquired on context switches and scheduler‑state access. The garbage collector is stop‑the‑world and runs at allocation points. gc-threshold controls how much allocation is tolerated before the next collection.
Likely explanation: when a thread allocates enough to cross the GC threshold while it already holds the scheduler lock, the collector must wait for the lock to be released before it can pause all threads. That adds the lock‑hold time to the pause. Under heavy concurrency the chance of this overlap rises with both the number of threads and the per‑thread allocation rate.
gc-threshold; there is no formula that maps a specific allocation rate to a guaranteed pause.Isolate the effect by removing other pause sources and comparing allocation‑heavy vs. allocation‑light workloads. A safe, scoped benchmark looks like this:
(require racket/gc)
(parameterize ([gc-threshold 0.5])
(gc-stats #t)
(define N 50)
(define threads
(for/list ([i (in-range N)])
(thread
(lambda ()
(let loop ()
(define t0 (current-inexact-milliseconds))
(make-bytes 100000) ; allocation-heavy work
(yield)
(define t1 (current-inexact-milliseconds))
(when (> (- t1 t0) 5.0)
(printf "thread ~a pause ~a ms\n" i (- t1 t0)))
(loop))))))
(for ([t (in-list threads)]) (thread-wait t)))
Repeat with (parameterize ([gc-threshold 0.8]) …) and verify that pause frequency drops. If pauses shrink only in the allocation‑heavy case, the scheduler‑lock/GC interaction is the likely source.
gc-threshold to reduce GC frequency at the cost of higher memory use.Please confirm the exact Racket version you are running. Because the default gc-threshold and internal GC heuristics can vary by release, the version determines whether the approximate 0.5 factor applies and whether tuning gc-threshold is the right first step.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.