Event loop blocking limits during CPU-intensive go-blocks
0 reputation · 08 Oct 2021, 02:48 UTC
0 reputation · 08 Oct 2021, 02:48 UTC
ClojureScript relies on the single-threaded event loop of the JavaScript host environment. While core.async provides go-blocks to manage asynchronous flows via state machines, these blocks are designed to yield during channel operations rather than preempting long-running synchronous computations.
When a go-block contains a heavy CPU-bound task—such as a complex data transformation or a large loop—without an intervening or channel operation, the execution remains synchronous on the main thread. This behavior introduces latency for all other concurrent requests or UI events until the computation completes.
Given these execution constraints, what are the established patterns for ensuring a long-running synchronous process within a go-block does not starve the event loop? Is there a documented mechanism to force a yield to the host environment without manually inserting channel-based delays?
34275 reputation · 08 Oct 2021, 11:24 UTC
No, core.async does not provide a built-in preemptive yield primitive. A go-block only relinquishes control at explicit asynchronous boundaries—channel takes (<!), puts (>!), alt!, or timeout. Pure synchronous CPU work inside a go-block runs to completion on the host’s single thread, blocking all other tasks, timers, and UI updates.
In ClojureScript, go-blocks compile to state machines executed on the JavaScript main thread. The state machine advances only when the code reaches a channel operation that can park. Between those points, execution is synchronous and uninterruptible. There is no scheduler tick, no yield keyword, and no documented API to force a cooperative yield without introducing an asynchronous boundary.
WebWorker. Serialize input, postMessage, receive result via promise/channel bridge.worker_threads with MessagePort or a promise-based wrapper.If the work must stay in-process, break it into small chunks and insert a zero-delay timeout between chunks:
(go
(doseq [chunk (partition 1000 large-coll)]
(process-chunk chunk)
(<! (async/timeout 0)))) ; yields to event loop
Each (async/timeout 0) parks the state machine, returns control to the host, and resumes on the next microtask/macrotask tick. Tune chunk size so no single chunk blocks longer than ~16 ms (one frame).
requestIdleCallback (Browser) or setImmediate (Node)For background work that can tolerate deferral, wrap chunks in a promise resolved by requestIdleCallback and <! the promise via async/promise-chan. This lets the browser prioritize UI work.
| Constraint | Preferred Approach |
|---|---|
| Work > 50 ms, independent data | Web Worker / worker_threads |
| Work 1–50 ms, must share ClojureScript state | Chunk + (<! (async/timeout 0)) |
| Low-priority background, browser only | requestIdleCallback + promise-chan |
js/setTimeout logging timestamps; observe gaps during the heavy go-block.perf_hooks.monitorEventLoopDelay() to quantify lag.Are you targeting the browser or Node.js? The offloading APIs (Web Workers vs. worker_threads) and idle-callback availability differ, so the exact implementation changes.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.