How Windows 95 Handled Win32 Multithreading
Windows 95 represented a major architectural shift for personal computing by introducing true preemptive multithreading for 32-bit applications running under the Win32 API. Unlike its predecessor Windows 3.1, which relied entirely on cooperative multitasking, Windows 95 delegated execution scheduling to the operating system kernel, enabling multiple threads within a single Win32 process to execute concurrently. This article explores the mechanics of how the Windows 95 kernel scheduled threads, handled context switching, managed thread synchronization, and balanced 32-bit execution alongside legacy 16-bit architectural constraints.
The Win32 Thread Model
In the Win32 API, a process is an organizing container that owns a
private virtual address space, system handles, and security contexts,
but it does not execute code directly. Instead, execution is handled
strictly by threads. A Win32 application starts with a single primary
thread created automatically by the operating system, but it can spawn
additional concurrent threads using the CreateThread API
function.
Under Windows 95, every Win32 thread is an independent unit of execution recognized by the operating system’s core scheduler. Each thread maintains its own:
- CPU register state (including the instruction pointer).
- Kernel-mode and user-mode stacks.
- Thread Local Storage (TLS) array.
- Thread Execution Block (TEB) in user memory.
Preemptive Scheduling and Context Switching
Windows 95 replaced cooperative multitasking with preemptive
scheduling for Win32 threads. The Virtual Machine Manager (VMM32) and
the underlying scheduler in KERNEL32.DLL controlled CPU
allocation using hardware interrupts.
At regular intervals, the programmable interrupt timer triggered a CPU interrupt that transferred control to the scheduler. The scheduler evaluated the currently running thread and determined whether its allocated time slice (quantum) had expired. If another thread of equal or higher priority was ready to execute, the operating system initiated a context switch:
- State Preservation: The processor saved the current thread's register states, stack pointer, and instruction pointer into its context structure.
- Context Loading: The scheduler selected the next ready thread from the scheduling queue and restored its register set.
- Address Space Switching: If the incoming thread belonged to a different process, the memory management unit updated the CR3 register to point to the new process's page directory, switching the 4 GB virtual address space. If the thread belonged to the same process, this page-table swap was bypassed, making intra-process thread switching significantly faster.
Priority Levels and Dynamic Boosting
Windows 95 utilized a priority-based, round-robin scheduler comprising 32 priority levels (0 to 31):
- Levels 0–15: Variable (dynamic) priority levels assigned to standard user applications.
- Levels 16–31: Real-time priority levels reserved for time-critical system functions.
Developers assigned priorities by selecting a process priority class
(such as IDLE_PRIORITY_CLASS,
NORMAL_PRIORITY_CLASS, or HIGH_PRIORITY_CLASS)
and modifying individual threads relative to that class using
SetThreadPriority.
To maintain user interface responsiveness, the Windows 95 scheduler applied dynamic boosting. When an application's thread received user input (such as keystrokes or mouse clicks) or completed an I/O operation, the scheduler temporarily boosted the thread's priority level. The thread would then gradually decay back to its baseline priority as it consumed CPU cycles.
Synchronization Primitives
Because multiple threads could attempt to modify the same resources simultaneously, the Win32 subsystem on Windows 95 provided native synchronization objects:
- Critical Sections: Fast, user-mode objects used exclusively for synchronizing threads within the same process.
- Mutexes, Semaphores, and Events: Kernel-level
objects handled via
CreateMutex,CreateSemaphore, andCreateEvent.
Threads called blocking functions like
WaitForSingleObject or WaitForMultipleObjects.
When a thread blocked waiting on an unsignaled object, the scheduler
transitioned it from the "ready" state to the "waiting" state,
immediately yielding the remainder of its time slice to other active
threads without wasting CPU cycles.
The Win16 Lock (Win16Mutex)
While Windows 95 provided preemptive scheduling for Win32 threads, it
was a hybrid 16-bit/32-bit operating system. Significant portions of the
user interface (USER.EXE) and graphics system
(GDI.EXE) remained non-reentrant 16-bit code to maintain
compatibility with Windows 3.1 software and reduce memory usage.
To prevent corruption, Windows 95 introduced a global mutual
exclusion semaphore known as the Win16Mutex (or "Win16
Lock"). Whenever a preemptive 32-bit thread called an API function that
descended into the 16-bit USER or GDI code
layers, it was forced to acquire the Win16Mutex:
- If another thread (16-bit or 32-bit) already held the lock, the calling thread stalled until the lock was released.
- If a 16-bit application yielded cooperatively while holding the lock, 32-bit threads attempting to make graphical calls were blocked.
Computational Win32 threads that did not make calls to the 16-bit GUI subsystem were unaffected by this bottleneck and continued to execute with true preemption. This hybrid design enabled Windows 95 to deliver modern multithreaded capabilities while running efficiently on the resource-constrained hardware of its era.