Skip to content

Conversation

@glennawatson
Copy link
Contributor

Summary

Remove ThreadStatic from RxSchedulers.MainThreadScheduler and RxSchedulers.TaskpoolScheduler so that schedulers are process-wide singletons instead of thread-local values.

Motivation / Context

When RxSchedulers was introduced, both MainThreadScheduler and TaskpoolScheduler were marked [ThreadStatic]. Unlike the pre-split RxApp (where only the unit test fields were thread-static), this made the production schedulers thread-local. The result:

  • Accessing RxSchedulers.MainThreadScheduler/TaskpoolScheduler from different threads could return different instances.
  • Switching threads (or running in environments with multiple app domains/contexts) could cause the value to “reset” back to defaults, leading to subtle behavior changes and flakiness.

This regressed the previous behavior where production schedulers were app-global, and only unit-test overrides were thread-scoped.

What’s changed

  • Removed [ThreadStatic] from:

    private static volatile IScheduler? _mainThreadScheduler;
    private static volatile IScheduler? _taskpoolScheduler;
  • Retained the existing lock-based lazy init to ensure thread-safe, single-instance initialization:

    • MainThreadScheduler defaults to DefaultScheduler.Instance.
    • TaskpoolScheduler defaults to TaskPoolScheduler.Default (or DefaultScheduler.Instance in PORTABLE).
  • No public API changes.

Current behavior (before this PR)

  • RxSchedulers.MainThreadScheduler / TaskpoolScheduler are thread-local: each thread can see an independent value, and new threads may “re-initialize” to default unexpectedly.
  • Tests or apps that set the scheduler on one thread can observe a different scheduler on other threads.

New behavior (after this PR)

  • RxSchedulers.MainThreadScheduler / TaskpoolScheduler are process-wide singletons:

    • Set once, visible consistently across all threads.
    • No unexpected resets when code runs on a different thread.
  • RxApp semantics remain unchanged:

    • Unit test overrides are still isolated via thread-static fields inside RxApp only, preserving historical test behavior.

Risks / Breaking changes

  • Very low. The change restores the pre-split behavior and aligns RxSchedulers with developer expectations (global, stable schedulers in production code).
  • Code that relied (intentionally or accidentally) on the thread-local behavior of RxSchedulers may observe different (correct) behavior now; this is considered a bug fix.

How I verified

  • Manual sanity checks:

    • Set RxSchedulers.MainThreadScheduler on a background thread; read from UI/main thread → same instance.
    • Spawn multiple threads reading/writing schedulers concurrently → stable value, no races (protected by lock).
  • Ensured RxApp unit-test behavior remains intact (thread-static kept where it was originally: _unitTest* fields).

Repro (old bug)

// On thread A
RxSchedulers.MainThreadScheduler = new TestScheduler();

// On thread B
var s = RxSchedulers.MainThreadScheduler; // Before: could be DefaultScheduler (reset). After: TestScheduler.

Documentation impact

  • None for public API. Internal behavior now matches historical RxUI expectations:

    • Use RxSchedulers for simple, app-global schedulers.
    • Use RxApp when you need test-runner detection and per-thread unit test overrides.

Related

  • Regression was introduced when splitting schedulers into RxSchedulers: production fields gained [ThreadStatic], diverging from the original RxApp pattern where only unit test fields were thread-static.

Checklist

  • Bug fix (no breaking public API changes)
  • Behavior aligned with pre-split RxApp
  • Tests added:

Removed ThreadStatic attribute from mainThreadScheduler and taskpoolScheduler.
@glennawatson
Copy link
Contributor Author

Related to reactiveui/ReactiveUI.Avalonia#27

@glennawatson glennawatson changed the title fix: Fix Schedulers on different threads causing issues fix: ensure consistent schedulers across threads by removing ThreadStatic in RxSchedulers Oct 23, 2025
@glennawatson glennawatson merged commit c3d6d99 into main Oct 24, 2025
9 of 12 checks passed
@glennawatson glennawatson deleted the glennawatson-patch-1 branch October 24, 2025 11:07
@giard-alexandre
Copy link

FYI, this should fix an issue that I totally opened in the wrong repo dotnet/reactive#2262

@glennawatson
Copy link
Contributor Author

Yeah, I'll do a release for this tomorrow. @giard-alexandre

the dotnet/reactive team are completely different people to ReactiveUI so definitely could be confusing to them :)

@giard-alexandre
Copy link

Yeah my bad. I meant for it to be in this Repo hahaha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants