Skip to content

UI thread and schedulers

Run the complete page example.

A sequencer is ReactiveUI's word for the thing an operator schedules work onto — a UI dispatcher, the thread pool, or a test clock. RxSchedulers.MainThreadScheduler is the sequencer for the thread that owns your views. Set a property a view binds to from any other thread, and most XAML platforms throw or corrupt the view.

Marshal at the boundary

WitnessOn(RxSchedulers.MainThreadScheduler) marks the point where a value crosses onto the main thread. Everything after it runs there; everything before it can run wherever the source produced the value.

PreferWitnessOnAtTheBoundary fetches a grade on a background thread, then uses WitnessOn right before the Subscribe that sets the bound property:

using IDisposable subscription = Signal.FromAsync(FetchLatestGradeAsync)
    .WitnessOn(RxSchedulers.MainThreadScheduler)
    .Subscribe(grade =>
    {
        student.Grade = grade;
        completion.SetResult();
    });

Without it, the subscription still runs here, since a console has no UI thread to protect. The same code on a UI platform can crash or corrupt the view:

Student student = new("Ada", "Robotics Club");
int grade = await FetchLatestGradeAsync();
student.Grade = grade;

Both print the same value here:

91

Pass the scheduler to the operation instead

For an operation you write yourself, passing the sequencer in lets the operation deliver its own result there, instead of adding a separate WitnessOn step afterward. PreferPassingTheSchedulerToTheOperation calls a fetch that takes an ISequencer and schedules its own continuation on it:

int grade = await FetchLatestGradeOnAsync(RxSchedulers.MainThreadScheduler);
student.Grade = grade;

This is worth doing for a longer-running or multi-step operation. It removes a WitnessOn step that would otherwise sit between two steps that already run on the target sequencer.

Only one WitnessOn, at the boundary

Adding WitnessOn after every step in a chain is a common instinct when chasing down a threading crash. It stops the crash, but schedules more work onto the main thread than the chain needs. See only one WitnessOn per chain for the same chain with three WitnessOn calls against the one that needs only one.

At a glance

DoInstead ofWhy
WitnessOn(RxSchedulers.MainThreadScheduler) right before SubscribeSubscribing with no marshalingThe update lands on the thread the view expects.
Pass RxSchedulers.MainThreadScheduler into a multi-step operationA separate WitnessOn after it returnsOne scheduling hop instead of two.