Loading content...
A process running at NORMAL_PRIORITY_CLASS might contain dozens of threads with vastly different responsiveness requirements. A word processor's main UI thread must respond instantly to keystrokes, while a background spell-checking thread can afford delays measured in seconds. A video player's audio decoding thread is more latency-sensitive than the subtitle parsing thread. How does Windows handle these differing requirements within a single process?
The answer lies in thread priority levels—the second tier of the Windows priority architecture. While priority classes establish the process-wide baseline, priority levels allow individual threads to operate above or below that baseline. This two-tier system provides remarkable flexibility: a process can have critical threads that preempt threads in higher-priority-class processes, and helper threads that yield even to lower-priority-class processes.
By the end of this page, you will master the seven Windows thread priority levels, understand how they combine with priority classes to compute base priority, learn the APIs for manipulating thread priority, and develop the judgment to choose appropriate priority levels for different thread types in your applications.
Windows defines seven symbolic priority levels that threads can use relative to their process's base priority. These levels translate to numerical offsets that are added to (or subtracted from) the process base priority to determine the thread's final base priority.
The priority levels, ordered from lowest to highest:
| Priority Level | Constant | Offset | Description |
|---|---|---|---|
| Idle | THREAD_PRIORITY_IDLE | Special | Runs only when no other thread is runnable. Maps to priority 1 (or 16 in realtime class) |
| Lowest | THREAD_PRIORITY_LOWEST | -2 | Two levels below the process base priority |
| Below Normal | THREAD_PRIORITY_BELOW_NORMAL | -1 | One level below the process base priority |
| Normal | THREAD_PRIORITY_NORMAL | 0 | Same as the process base priority (default for new threads) |
| Above Normal | THREAD_PRIORITY_ABOVE_NORMAL | +1 | One level above the process base priority |
| Highest | THREAD_PRIORITY_HIGHEST | +2 | Two levels above the process base priority |
| Time Critical | THREAD_PRIORITY_TIME_CRITICAL | Special | Highest possible priority. Maps to priority 15 (or 31 in realtime class) |
Understanding the offsets:
For most priority levels, the offset is applied directly to the process base priority:
Thread Base Priority = Process Base Priority + Level Offset
For example, in a NORMAL_PRIORITY_CLASS process (base 8):
Special cases: Idle and Time-Critical:
The Idle and Time-Critical levels are special—they don't use offsets. Instead, they map to fixed priority values:
This creates natural "floor" and "ceiling" values within each priority range.
Priority calculations saturate at their range boundaries. A HIGH_PRIORITY_CLASS process (base 13) with THREAD_PRIORITY_HIGHEST would calculate to 15, which is valid. But the system won't let thread priorities leave their allowed range—dynamic threads stay in 1-15, realtime threads stay in 16-31.
The combination of 6 priority classes and 7 priority levels produces a matrix of possible base priorities. Understanding this matrix is essential for predicting thread scheduling behavior.
Dynamic priority range (priority classes Idle through High):
| Priority Class | Idle | Lowest | Below Normal | Normal | Above Normal | Highest | Time-Critical |
|---|---|---|---|---|---|---|---|
| Idle (base 4) | 1 | 2 | 3 | 4 | 5 | 6 | 15 |
| Below Normal (base 6) | 1 | 4 | 5 | 6 | 7 | 8 | 15 |
| Normal (base 8) | 1 | 6 | 7 | 8 | 9 | 10 | 15 |
| Above Normal (base 10) | 1 | 8 | 9 | 10 | 11 | 12 | 15 |
| High (base 13) | 1 | 11 | 12 | 13 | 14 | 15 | 15 |
Key observations:
All Time-Critical threads map to 15: Regardless of the process's priority class, Time-Critical threads converge at priority 15—the highest dynamic priority.
All Idle threads map to 1: Except in Realtime processes, Idle threads converge at priority 1—just above the zero-page thread.
Overlapping ranges: A HIGH_PRIORITY_CLASS process's lowest-priority thread (11) still outranks a NORMAL_PRIORITY_CLASS process's highest-priority thread (10). This strict hierarchy is intentional.
Dynamic priorities stay below 16: All threads in the Idle through High priority classes remain in the 1-15 range, leaving 16-31 for Realtime.
Realtime priority range:
| Priority Class | Idle | -7 | -6 | -5 | -4 | -3 | ... | +6 | Time-Critical |
|---|---|---|---|---|---|---|---|---|---|
| Realtime (base 24) | 16 | 17 | 18 | 19 | 20 | 21 | ... | 30 | 31 |
Realtime priority levels:
In the Realtime priority class, the priority level offsets work differently. Instead of five levels (-2 to +2), threads can specify priorities from 16 to 31 using:
This provides full access to all 16 realtime priority levels (16-31) for applications requiring fine-grained control within the realtime range.
Windows provides straightforward APIs for manipulating thread priority levels. Unlike priority classes, thread priority changes typically don't require special privileges (except for realtime priorities exceeding 15).
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
#include <windows.h>#include <iostream>#include <thread> // Setting the current thread's priority levelvoid SetCurrentThreadPriorityExample() { // Elevate to Above Normal to improve responsiveness BOOL success = SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL ); if (success) { std::cout << "Thread priority set to Above Normal\n"; } else { std::cerr << "Failed to set thread priority: " << GetLastError() << "\n"; }} // Querying a thread's priority levelint QueryThreadPriorityExample(HANDLE hThread) { int priority = GetThreadPriority(hThread); if (priority == THREAD_PRIORITY_ERROR_RETURN) { std::cerr << "Failed to get thread priority\n"; return -999; } // Decode the symbolic name const char* levelName = "Unknown"; switch (priority) { case THREAD_PRIORITY_IDLE: levelName = "Idle"; break; case THREAD_PRIORITY_LOWEST: levelName = "Lowest"; break; case THREAD_PRIORITY_BELOW_NORMAL: levelName = "Below Normal"; break; case THREAD_PRIORITY_NORMAL: levelName = "Normal"; break; case THREAD_PRIORITY_ABOVE_NORMAL: levelName = "Above Normal"; break; case THREAD_PRIORITY_HIGHEST: levelName = "Highest"; break; case THREAD_PRIORITY_TIME_CRITICAL: levelName = "Time Critical"; break; } std::cout << "Thread priority level: " << levelName << " (" << priority << ")\n"; return priority;} // Creating a thread with a specific priorityDWORD WINAPI WorkerThread(LPVOID param) { // Worker immediately adjusts its own priority SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); // Perform background work... std::cout << "Worker thread running at Below Normal\n"; Sleep(1000); return 0;} void CreateThreadWithPriorityExample() { HANDLE hThread = CreateThread( NULL, 0, WorkerThread, NULL, CREATE_SUSPENDED, // Start suspended to set priority first NULL ); if (hThread) { // Set priority before resuming SetThreadPriority(hThread, THREAD_PRIORITY_BELOW_NORMAL); // Now resume execution ResumeThread(hThread); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); }} // Setting priority for a thread in another processvoid SetRemoteThreadPriority(DWORD processId, DWORD threadId) { // First, open the thread with required access HANDLE hThread = OpenThread( THREAD_SET_INFORMATION | THREAD_QUERY_INFORMATION, FALSE, threadId ); if (hThread) { // Attempt to change priority if (SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST)) { std::cout << "Remote thread priority changed\n"; } else { std::cerr << "Failed: " << GetLastError() << "\n"; } CloseHandle(hThread); }}Creating suspended threads:
A common pattern is to create threads in a suspended state, set their priority, then resume them. This prevents race conditions where a thread briefly runs at the wrong priority:
CreateThread with CREATE_SUSPENDED flagSetThreadPriority to desired levelResumeThread to begin executionThis ensures the thread never runs at the default Normal priority if you intend a different level.
Thread-pool considerations:
When using the Windows Thread Pool (via SubmitThreadpoolWork, CreateThreadpoolWork, etc.), you don't have direct control over thread creation. Instead:
SetThreadpoolCallbackPool with custom pools for priority controlSetThreadpoolThreadMaximum/Minimum for capacity managementC++11's std::thread doesn't expose priority control. To set priority for a std::thread, obtain its native handle with thread.native_handle() and pass it to SetThreadPriority(). Note that this is Windows-specific and not portable.
Different application architectures call for different thread priority arrangements. Here are battle-tested patterns used in production systems:
When threads of different priorities share resources (locks, queues), priority inversion can occur: a high-priority thread waits for a low-priority thread that can't run because medium-priority threads consume all CPU time. Windows addresses this with priority inheritance and priority boosts, covered in the next page.
While we've focused on base priority (the static computation from class + level), Windows actually schedules threads according to their current priority, which may differ from base priority due to dynamic adjustments.
The distinction:
Base Priority: Calculated from priority class + priority level. This is the "home" priority that the thread always returns to.
Current Priority: The actual scheduling priority in use. May be temporarily higher than base priority due to boosts.
Automatic adjustments:
Windows automatically adjusts current priority based on:
Priority decay:
After a boost, a thread's current priority decays toward its base priority. With each quantum expiration:
if (current_priority > base_priority) {
current_priority--;
}
This continues until current_priority == base_priority, at which point the thread operates at its natural priority.
Observing dynamic priority:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
#include <windows.h>#include <iostream>#include <TlHelp32.h> // Get thread's current (dynamic) priority using toolhelpvoid ObserveThreadPriorities(DWORD processId) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hSnapshot == INVALID_HANDLE_VALUE) return; THREADENTRY32 te = { sizeof(te) }; if (Thread32First(hSnapshot, &te)) { do { if (te.th32OwnerProcessID == processId) { HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, te.th32ThreadID); if (hThread) { // GetThreadPriority returns the LEVEL, not current priority int level = GetThreadPriority(hThread); // Note: There's no direct API for current priority! // It's visible in toolhelp and performance counters // te.tpBasePri is the thread's base priority std::cout << "Thread " << te.th32ThreadID << " base priority: " << te.tpBasePri << " level: " << level << "\n"; CloseHandle(hThread); } } } while (Thread32Next(hSnapshot, &te)); } CloseHandle(hSnapshot);} // Disable priority boosting for a threadvoid DisablePriorityBoost(HANDLE hThread) { BOOL disableBoost = TRUE; SetThreadPriorityBoost(hThread, disableBoost); // Thread will no longer receive automatic priority boosts} // Query if priority boosting is enabledvoid CheckPriorityBoostStatus(HANDLE hThread) { BOOL boostDisabled; if (GetThreadPriorityBoost(hThread, &boostDisabled)) { std::cout << "Priority boosting is " << (boostDisabled ? "DISABLED" : "ENABLED") << "\n"; }}Threads in the Realtime priority class (base priority 16-31) never receive priority boosts and never experience priority decay. Their current priority always equals their base priority. This determinism is essential for real-time applications that need predictable timing.
Let's examine the complete set of thread priority-related APIs and their behaviors:
| API Function | Purpose | Notes |
|---|---|---|
| SetThreadPriority | Set thread's priority level | Fails for realtime levels without SE_INC_BASE_PRIORITY_NAME |
| GetThreadPriority | Get thread's priority level | Returns symbolic level, not numeric priority |
| SetThreadPriorityBoost | Enable/disable priority boosting | Disabled boost = more predictable but potentially less responsive |
| GetThreadPriorityBoost | Query boost status | Returns TRUE if boosting is disabled |
| SetThreadIdealProcessor | Hint preferred processor | Combined with priority for affinity-aware scheduling |
Extended APIs for modern Windows:
Windows 11 and Server 2022 introduce additional priority controls through the SetThreadInformation API:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
#include <windows.h>#include <processthreadsapi.h> // Set I/O priority alongside CPU priority (Windows Vista+)void SetLowIOPriority(HANDLE hThread) { // Thread I/O priority hints THREAD_PRIORITY_HINT hint = ThreadPriorityHintLow; SetThreadInformation( hThread, ThreadPriorityHint, &hint, sizeof(hint) );} // Set memory priority for better memory resource allocationvoid SetLowMemoryPriority(HANDLE hThread) { MEMORY_PRIORITY_INFORMATION memInfo = { 0 }; memInfo.MemoryPriority = MEMORY_PRIORITY_LOW; SetThreadInformation( hThread, ThreadMemoryPriority, &memInfo, sizeof(memInfo) );} // Power throttling for background work (Windows 10 1709+)void EnablePowerThrottling(HANDLE hThread) { THREAD_POWER_THROTTLING_STATE throttle = { 0 }; throttle.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION; throttle.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; throttle.StateMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; SetThreadInformation( hThread, ThreadPowerThrottling, &throttle, sizeof(throttle) ); // Thread may be scheduled on efficiency cores // and run at reduced frequency} // Comprehensive background thread setupvoid ConfigureBackgroundThread(HANDLE hThread) { // CPU: Below Normal SetThreadPriority(hThread, THREAD_PRIORITY_BELOW_NORMAL); // I/O: Low priority FILE_IO_PRIORITY_HINT_INFO ioHint = { IoPriorityHintLow }; SetFileInformationByHandle(hThread, FileIoPriorityHintInfo, &ioHint, sizeof(ioHint)); // Memory: Low priority MEMORY_PRIORITY_INFORMATION memInfo = { MEMORY_PRIORITY_VERY_LOW }; SetThreadInformation(hThread, ThreadMemoryPriority, &memInfo, sizeof(memInfo)); // Power: Allow throttling THREAD_POWER_THROTTLING_STATE throttle = { THREAD_POWER_THROTTLING_CURRENT_VERSION, THREAD_POWER_THROTTLING_EXECUTION_SPEED, THREAD_POWER_THROTTLING_EXECUTION_SPEED }; SetThreadInformation(hThread, ThreadPowerThrottling, &throttle, sizeof(throttle));}Windows 11 introduces EcoQoS (Efficiency Mode), visible in Task Manager. It combines power throttling, lower memory priority, and hints to the scheduler that the thread should be scheduled on efficiency cores (on hybrid processors like Intel 12th Gen+). This is the recommended approach for truly background work in modern Windows.
For audio/video applications, use the Multimedia Class Scheduler Service (MMCSS) instead of raw priority manipulation. MMCSS dynamically boosts threads during multimedia playback and restores them during idle periods. Use AvSetMmThreadCharacteristics("Pro Audio", ...) to register with MMCSS.
Thread priority levels provide the fine-grained control that allows sophisticated multi-threaded applications to coordinate diverse workloads within a single process.
SetThreadInformation provides comprehensive control.What's next:
We've seen that Windows automatically boosts thread priorities under certain conditions. The next page provides a deep dive into priority boosting—the mechanisms Windows uses to improve responsiveness, prevent starvation, and address priority inversion. Understanding these automatic adjustments is essential for predicting how your threads will actually behave in the real world.
You now understand how thread priority levels combine with priority classes to produce the final base priority for each thread. With this knowledge, you can design multi-threaded applications where critical threads receive preferential scheduling while background threads yield appropriately—all within the Windows priority architecture.