When the scheduler you expected is not the scheduler you have
A program is not just code — it is code shaped around a particular scheduler. The thread priorities it sets, the spin-loops it tolerates, the places it yields and the places it does not: all of these reflect decisions made against a target operating system's scheduling behaviour. Run that program through a compatibility layer and the syscall boundary passes the thread calls across, but the semantics on the other side may be quietly different.
The scheduling difference in numbers
FROM THIS ENTRY| Windows default thread quantum | approximately 15 ms on a desktop; shorter with foreground boost active |
|---|---|
| Linux CFS | tracks virtual runtime for proportional distribution, not fixed quanta |
| Windows priority classes | named constants mapping to numerical values with direct preemption influence; POSIX priorities require elevated capabilities to raise on Linux |
The gap that looks like nothing
Windows uses a priority-based preemptive scheduler with a default quantum of roughly 15 milliseconds on a desktop build — shorter when foreground-boost is active. Linux's Completely Fair Scheduler operates on a different model entirely: it tracks virtual runtime and aims to distribute CPU time proportionally rather than by fixed quanta. A program that spins briefly on a mutex because experience says the owning thread will be rescheduled within a few milliseconds may spin far longer under CFS, burning a core and starving something else. The compatibility layer translated the call correctly; the problem is in what the program assumed about the answer.
Thread priority is a related trap. Windows exposes a range of named priority classes that map onto numerical values and influence preemption directly. POSIX priorities interact differently with the scheduler, and unprivileged processes on Linux cannot raise their own scheduling priority without elevated capabilities. A game that bumps its render thread to THREAD_PRIORITY_HIGHEST may find that call silently succeeding — the layer accepts it, converts it, passes it on — while the actual effect on CPU time is negligible. No error is returned. The frame-time consequence shows up only in a profiler.
Futex behaviour adds another layer. Windows WaitOnAddress and Linux futexes are conceptually similar but not identical in their spurious-wakeup and ordering guarantees. A translation layer implements the mapping as faithfully as it can, but subtly different wake ordering under contention can shift which thread runs next, turning a tuning decision into a latency source that is genuinely hard to isolate.
None of this is mistranslation in the strict sense. The API calls are handled; the contracts are met. The invisible assumption — that the machine underneath will schedule the way the machine it was written for scheduled — is simply no longer true. That gap does not announce itself. It accumulates in where the milliseconds go.


