ADR 0002: Reactive Thread-Pool Scheduling
Status
Accepted.
Date
2026-05-30.
Context
Onity.Reactive already had SelectAwait and WhereAwait, which run callbacks through Task.Run, and Unity frame streams exposed OnityUnityThreadMode options. That left an API gap: users could not explicitly choose a managed thread-pool hop or run CPU-bound pure C# reactive work with configurable parallelism.
Unity Job/Burst is not a replacement for this gap. Managed reactive observers, closures, Subject<T>, ReactiveProperty<T>, and DI-backed services are managed object graphs and cannot execute inside Burst jobs. At the same time, pure managed CPU work is a valid use case for .NET thread-pool scheduling as long as UnityEngine API access is kept on the main thread.
Decision
Add managed thread-pool scheduling to Onity.Reactive as engine-free core API:
ObserveOnThreadPool<T>()re-posts source values onto a .NET thread-pool worker and preserves source order.SelectOnThreadPool<TSource, TResult>(...)runs CPU-bound selectors on the .NET thread pool with configurable max concurrency.
Keep Unity main-thread re-marshalling as a separate bridge concern through ObserveOnMainThread() in Onity.Unity.Reactive.
Keep OnityUnityThreadMode.JobMultiThread, BurstJobMultiThread, and DotsEventDriven as Unity frame-stream boundaries. They do not imply managed operator execution inside jobs or Burst.
Alternatives Considered
Expand OnityUnityThreadMode into managed operator scheduling
Rejected.
OnityUnityThreadMode is specific to Unity frame streams. Reusing it for core managed operators would couple Onity.Reactive to Unity concerns and blur the main-thread safety rule.
Run managed observers inside Unity Jobs or Burst
Rejected.
Managed delegates, closures, arbitrary reference types, and DI-resolved services are not Burst-safe. This would either fail technically or force Onity to expose a separate data-oriented API that is no longer the same reactive surface.
Only document SelectAwait as the threading path
Rejected.
SelectAwait is sequential and async-oriented. It is useful for one-at-a-time async workflows, but it is not explicit enough for CPU-bound concurrent work.
Consequences
Onity.Reactiveremains engine-free and keepsnoEngineReferences: true.- Thread-pool operators allocate and schedule work by design; they are opt-in and not part of the allocation-free synchronous hot path.
- Downstream observers after thread-pool operators run off the Unity main thread. Gameplay/UI chains must call
ObserveOnMainThread()before touching UnityEngine APIs. SelectOnThreadPoolemits in completion order when max concurrency is greater than one. Users who require source order should passmaxConcurrency: 1or useSelectAwait.- Job/Burst/DOTS work remains appropriate for blittable bridge workloads, not managed reactive observer execution.
Verification
- EditMode tests cover ordered
ObserveOnThreadPooldelivery, concurrentSelectOnThreadPoolexecution up to the configured limit, source-order preservation withmaxConcurrency: 1, and invalid max concurrency. dotnet build Onity.Reactive.csproj -nologopasses on Unity2022.3.62f3project files.