CPU-intensive operations
Ox runs forks on virtual threads, which are not preempted by the JVM’s scheduler: a virtual thread yields control only when it performs a blocking operation. A long-running, CPU-intensive computation which doesn’t call any blocking operations monopolizes a carrier thread of the virtual thread scheduler. Since there are as many carrier threads as CPU cores (by default), a handful of such computations can starve all other virtual threads in the process
including latency-sensitive ones.
There are two remedies, depending on the kind of computation:
for short computation bursts in code that you control, insert periodic
cede()calls, which yield the virtual thread back to the scheduler,for long-running computations, or code which can’t be instrumented with yields (e.g. third-party libraries), use
computeIntensive.
computeIntensive
computeIntensive(f) runs the computation f on a pool of platform
threads, blocking the calling (virtual) thread until it completes; the result is
returned (or the computation’s exception rethrown). Platform threads are
scheduled preemptively by the operating system, so however long the computation
runs, virtual threads keep making progress.
import ox.*
def expensive(): BigInt = (1 to 1_000_000).map(BigInt(_)).product
val result: BigInt = computeIntensive(expensive())
Because the caller blocks until the computation completes, computeIntensive is
structured: the computation never outlives the enclosing concurrency scope. To
run computations in parallel with other code, combine with forks:
import ox.*
def expensive(): BigInt = (1 to 1_000_000).map(BigInt(_)).product
supervised {
val f1 = fork(computeIntensive(expensive()))
val f2 = fork(computeIntensive(expensive()))
f1.join() + f2.join()
}
The compute pool
By default, computations run on a lazily-created global pool of daemon platform
threads, sized to the number of available processors. The pool can be replaced
application-wide using setOxComputeExecutor (before any computeIntensive
calls; also available as OxApp.Settings.computeExecutor), e.g. to use a
smaller pool in latency-sensitive deployments, reserving cores for the virtual
thread scheduler. A per-call executor can be given as well:
computeIntensive(executor)(f).
Nested computeIntensive calls targeting the same executor run inline, on the
current (pool) thread; calls targeting a different executor are submitted to
that executor.
Note that the lifecycle of a custom executor is the caller’s responsibility:
once it is shut down, further submissions fail with a
RejectedExecutionException, and tasks dropped by shutdownNow never start
(their callers keep waiting, until interrupted).
Cancellation
If the calling thread is interrupted - typically because the enclosing scope
ends, e.g. due to an error in another fork - the thread running the computation
becomes interrupted as well, and the caller keeps waiting until the computation
completes. That way, the computation never runs past the lifetime of its scope.
Computations should co-operate in the cancellation protocol by calling
checkInterrupt() (or cede()) periodically; a computation which doesn’t will
delay the scope’s shutdown until it completes. If the computation hasn’t yet
started when the cancellation occurs, it will never run.
Note that consequently, timeout(...)(computeIntensive(f)) can’t return before
the computation notices the interruption - with a non-cooperating computation,
the timeout will overshoot arbitrarily.
To return early on interruption instead of waiting, the call can be wrapped as
abandonOnInterrupt(computeIntensive(f))
note the precise semantics: the wait then runs on a detached thread which is never interrupted, so the computation runs to full completion, uninterrupted, holding a compute-pool slot even after the wait was abandoned.
Scope context
The scope context is not propagated to the computation: fork
locals read their default values
within f, and forks can’t be created there (this fails with an
IllegalStateException). The same applies to thread-local-based integrations,
such as MDC or OpenTelemetry contexts.