Thread Priorities are Evil

Programmers* love to futz around with thread priorities. As if programming with threads wasn't already dangerous enough, we've got to get in there and tweak thread priorities to make things run.. er.. "better".


This is a companion discussion topic for the original blog entry at: http://www.codinghorror.com/blog/2006/08/thread-priorities-are-evil.html

Jon,

Not only do media players (not just windows media player) run at a higher priority, they also set the timer resolution to ~1 msec. This happens for the entire system not just the application that set the resolution.

I do have to disagree with some points of this thread (excuse the pun). Threads that use 100% CPU are usually inidicatons of poorly written threads. In my experience, this occurs when the code is something like:

while(oktorun)
{
DoSomeWork()
}

In order to stop this thread, oktorun gets set to false. Bad, Bad, Bad.

As Jon points out, 1 solution to this is as follows:

while(oktorun)
{
DoSomeWork();
Sleep(1); //throw the slice
}

But, this is really not right either. IMHO, the Right Thing is as follows:
while(TRUE)
{
::WaitForSingleObject(blah,sometimeout);
DoSomeWork();
}

Then, when you need to shut down the thread, set the shutdown event. You could use WaitForMultpleObject as well.

How about setting the thread priority to realtime in Task Manager, that can be fun…

I sometimes set a conversion process to high priority because I need it to speed up. Then I regret it because it takes up so much that I can’t lower it again.

One poster has already mentioned this but I think it’s important enough to mention again. The problem is NOT with thread or process priorities, it is a problem with the Windows scheduling algorithm.

If my memory serves me (lately it often seems to have a different master it serves), Windows assigns all time slices to high priority processes UNTIL all are blocked and then it assigns time to normal processes until all those are blocked and then low prio processes get their shot. This is discussed in Advanced Windows by Jeffrey Richter (from Microsoft Press no less). Why MS built their AOS (Almost an Operating System) this way I have no idea.

Any decent textbook on operating systems (like the ones I read in my undergrad degree before Windows was a twinkle in Gates’ eye) will indicate that you shouldn’t completely starve lower priority tasks; simply allocate a much smaller pool of time slices to them than you allocate to higher priority tasks. Of course, this would not be what you would want to do in a real time system but I trust nobody is putting my life at risk by running a real time system on Windows.

Coleman: If the scheduler is doing it’s job, why would you need to constantly “give up” a time slice inside the loop? That’s a great way to introduce unnecessary context switching.

Attempting to usurp the responsibilities of the OS is usually not a good idea.

Now, if you were to argue that the scheduler is doing a poor job, that would be another thing entirely…

Don’t confuse thread priorities with process priorities. Threads inside processes can be scheduled with different priorities again.

But I do find priority tweaking useful; to down-schedule cpu-intensive, long-running tasks such as archiving and video encoding, so I can continue to use my desktop as usual. On a task that takes 15 minutes, I don’t mind waiting the extra minute if I can continue doing other stuff in the meantime.

An application that fits that bill has my blessing to set itself to BelowNormal priority. Otherwise, I haven’t found a good reason to mess with priorities yet, either.

Have a look at the code implementing all those nice UI animation effects, e.g. auto-hiding windows and such.

AFAIK, they all set the UI thread to high-prio, to have that smooth and steady feel to it. It’s only temporary, unless it somehow goes haywire, say by some incoming windows message.

I’m glad you started this topic as I’ve always wondered why pre-emptive multi-tasking doesn’t seem to work as I would expect it to.

Why should one task grab 100% and freeze out the UI and everything else?

We have an IIS server and if one page is busy for a long time why on earth should it freeze everyone else out and slow the whole server to a crawl?

I don’t understand why processing power is not spread equally between the other processes - I always assumed that is what PRE-EMPTIVE multitasking was supposed to do when it was launched in XP after Windows 95 et al.

There doesn’t seem to be any other resource blocks BTW…besides even if there was the I/O should also be shared shouldn’t it??

Quite a predictable result. I think problem is rather with design of inter-thread communication, not with priorities. Consumer should actually wait for producer. In this case scheduler not only can free CPU cycles by not giving them to consumer, but it can also boost producer priority (because high-priority thread waits for it).

When I first had access to the task manager I set my DVD player to a higher priority as it was stuttering. Whoops.

Took over my UI thread, and so I couldn’t move the mouse to hit ‘play’.

:S

Grant Johnson, Tim Dudra, anyone else criticising the Windows scheduler: you’re wrong. Windows does indeed run threads by priority. It is fully pre-emptive, but that word means something different from what you think it does. Pre-empting means that when something occurs that causes a higher-priority thread to become runnable than is currently executing on the current, or any, processor, the current thread’s run is ended early (with the scheduler noting how much of the quantum has been used, the thread getting the rest of its quantum next time) and the higher-priority thread beginning its run immediately, rather than having to wait until either the next timer tick or to the end of the current thread’s quantum. The pre-empted thread gets pushed onto the front of the queue at its priority level, so it will be the next thread to run once all higher-priority threads have blocked (see next paragraph).

Windows’ scheduler is a round-robin priority-based scheduler. What that means is that it will take into account the priority of all runnable threads on the system. It runs all threads at the highest priority level at which there are runnable threads in a round-robin fashion - running each thread in turn. It will only start to consider lower-priority threads when those higher-priority threads are no longer runnable, which basically means when they’re blocked waiting for something to happen. The exception is when it has more CPUs than high-priority threads - in this case it will find work for the otherwise-spare CPUs to do.

Multi-CPU (or {logical} core) scheduling is more tricky since Windows has both an affinity mask which governs which cores a thread is allowed to run on (which is also misused!), an ‘ideal’ core for each thread (set when the thread is created, rotating across the cores within a process, so thread 0 might be on CPU 0, then thread 1 will be on CPU 1, thread 2 on CPU 2, then 3 on CPU 0 again, and so on), and remembering the last core that a thread ran on. These last two settings are a hint to try to improve cache locality - with luck the thread’s code or data may still be in the core/processor’s cache. You might find that the two highest priority threads on a two-core system wouldn’t actually both run concurrently if they have the same ideal core and there’s a runnable lower-priority thread which has the other core as its ideal core.

Windows stores two priority values: a base priority, and a dynamic priority. The scheduling is done based on the dynamic priority - the base priority is a low water mark below which a thread will never go. When a thread that was blocked becomes runnable, its dynamic priority is boosted - the amount it is boosted by depends on the reason it becomes unblocked. For disk I/O it’s boosted by 1, for network or serial port activity by 2, for keyboard and mouse input by 6, and if the sound card needs more data, by 8. If it was waiting for an event or a semaphore, it’s normally by 1. There are special functions which can cause a higher boost for events - the event used in a critical section boosts the unblocking thread to the priority of the thread which left the critical section plus one. If a thread unblocks due to GUI activity its priority is boosted by 2.

Joe Duffy mentions the balance set manager thread which wakes up once per second for various management duties. One of those is to look for threads that are runnable but haven’t run for 4 seconds. It temporarily gets boosted to priority 15 (the highest non-real-time priority) then, after its quantum expires, drops back to its base priority. It only examines 16 threads on the ready queue at a time, and will only boost 10 threads in any pass, so if there are a lot of runnable threads, it may take some time for any given thread to get its boost.

Full details are in ‘Windows Internals, Fourth Edition’ by Mark Russinovich and David Solomon, which I recommend to any programmer. If abstractions leak, you’d best understand the leaks.

Windows hasn’t had the concept of ‘yield’ since Windows 3.1. 16-bit applications do still act this way with each other on Windows NT-family systems but the whole 16-bit environment is pre-emptable against 32-bit applications.

The main problems are threads which poll rather than blocking, and applications which are thread-happy. Polling is brain-dead. The operating system offers numerous ways to wait for changes to occur, and they should be used. Polling wastes laptop batteries, because the CPU cannot be put into a low power state, and often causes disks to be kept spinning because the polling thread’s code and data may be pulled back into memory if trimmed from the working set.

A thread-happy application is one which creates, and keeps runnable, more threads than can be actually serviced by the system’s processors. The system spends its time thrashing the processor’s cache rather than doing useful work. Put one of these at high priority and suddenly very little else will get done.

Mr. Dimmick,

You wrote: “Windows’ scheduler is a round-robin priority-based scheduler. What that means is that it will take into account the priority of all runnable threads on the system. It runs all threads at the highest priority level at which there are runnable threads in a round-robin fashion - running each thread in turn. It will only start to consider lower-priority threads when those higher-priority threads are no longer runnable, which basically means when they’re blocked waiting for something to happen. The exception is when it has more CPUs than high-priority threads - in this case it will find work for the otherwise-spare CPUs to do.”

Isn’t this EXACTLY what I said, although admittedly from a different angle? If a high priority thread is ready to run (i.e., they are not ALL blocked), then Windows runs it, pre-empting if necessary any lower priority threads. It will then run that high priority thread EXCLUSIVELY until it is joined by another high priority thread that can be run (in which case the two or more of them share all the time quantums) or it (and any that joined it) gets blocked. Six of one, a half dozen of the other. I say Potato, you say Spud. We need to practice our critical reading skills a little before jumping out and practicing our critical writing skills.

Whether or not you believe your explanation to be more correct due to some bizarre bit of subtlety, the scheduling algorithm is absurd because it can and does result in starvation of lower priority threads, threads that may be necessary to move the high priority threads onward instead of just cycling - and we know what happens with cycling, especially American cycling; drugs, lots of drugs and, as Mr. Mackey says “Drugs are bad, don’t do drugs”.

I agree with Iggi here. The correct moral should be that spinning is evil, not thread priorities. The OS provides sync objects to handle this situation. They will even handle the priority inversion by boosting the lower thread’s priority.

Code should never spin unless the duration is so short that its quicker to spin (and this is enforced with a timeout), or your code owns the CPU (embedded and/or realtime programming).

That’s funny – I have to run a CPU graph monitoring tool on OS X so I can tell if a process is pegging the CPU (easy to do these days on a 700MHz iBook). If it wasn’t for the graph, it would take me a while to realize the machine was running slow, because the scheduler on this platform apparently actually works.

Except I’ve pegged the CPU under windows and not had as much trouble as people are indicating, so I wonder if paging is the real root problem here. When a procress takes over all the RAM and has to page to disk, it gets hung on the I/O. So the scheduler tries to run another program, but since all the RAM is taken over, that has to reload its working set from disk too, so it hangs on paging as well. …and so on. Everything starts hitting paging, and everything grinds to a halt.

I have experienced the same thing Ethan did. On the Mac sometimes you “feel” something slow, check the Activity Monitor and boom… a process is taking up 100% cpu and not responding, but the whole OS keeps working.

And this is on my “little” G4 1.25 GB Powerbook with 512Mb RAM.

It’s really amazing. :slight_smile:

Don’t confuse thread priorities with process priorities. Threads inside processes can be scheduled with different priorities again.

Yes, I know, but they are functionally similar. Thread priority is actually a 16-bit number from 0-31 (where 0 is reserved for system use). Here’s a page that describes the relationship between application priority and thread priority in Windows:

http://www.microsoft.com/technet/prodtechnol/windows2000serv/reskit/prork/pred_ana_umwv.mspx?mfr=true

The names are process priority, and each number goes from highest thread priority (Real Time) to lowest (Idle)

Real Time – 31, 26, 25, 24, 23, 22
High – 15, 15, 14, 13, 12, 11, 1
Normal – 15, 10, 9, 8, 7, 6, 1
Idle – 15, 6, 5, 4, 3, 2, 1

I’ve pegged the CPU under windows and not had as much trouble as people are indicating, so I wonder if paging is the real root problem here.

The problem is usually some other I/O bottleneck. Like a dual-core 3GHz monster rig brought to its knees by a single unreadable CD-ROM, for example.

The correct moral should be that spinning is evil, not thread priorities.

There may be other fixes, but manually setting a thread priority is a recipe for problems. The OS itself rarely sets explicit priorities, and for good reason-- you should rarely need to!

T.E.D., I respectfully disagree with your statement that “[t]he correct moral should be that spinning is evil, not thread priorities.”

If you are writing LOW-LEVEL concurrent algorithms, spinning is often a requirement for good throughput/parallel speedups. A context switch is just about the worst thing you can do when you’re trying to scale up to tens or even hundreds of processors. Not only does a context switch cost in the neighborhood of 4,000 cycles, but it can have a cascading effect on your entire set of threads due to causality, effectively killing your scalability.

Using a simple spin-lock is quite effective in many such cases. The CLR’s synchronization primitives build on top of the OS primitives, adding some lightweight spinning up front. We have spent many years of work fine tuning these algorithms across 3 major releases, and countless machine architectures and OS SKUs. The result is that statistically we acheive better performance on multi-processor machines when compared to the same workloads using the OS primitives directly. This is, of course, workload dependent. If code holds a lock for a long period of time or spinning is used to wait for high latency events, then the thread will eventually block anyhow. And therefore, spinning just represents wasted CPU cycles.

OK… voluntarily giving up a time slice is not REALLY the worst thing you can do. A poorly written spin wait is even worse. Only a small subset of programmers should ever need to write one, but those that do need to know how to do it correctly. It turns out that most of the programmers I interact with daily are library developers at Microsoft, which means many of them have to face this head on. And so I admit my perspective may be a little skewed.

Regards, joe

jsm

Mike and Tim said it way better than I could have:

“It will only start to consider lower-priority threads when those higher-priority threads are no longer runnable, which basically means when they’re blocked waiting for something to happen.”

“…when they’re blocked…”

This is what sleep and WFSO/WFMO do, make the thread wait so that other processes get a turn. More precisely so that that scheduler will give some time to other processes and threads. If you have a while(true) loop that’s running “full tilt” you’re never giving the scheduler a chance to give time to other processes – which will lead to 100% CPU on single CPU systems. This happens whether the thread is high or normal priority.

You asked:
“If the scheduler is doing it’s job, why would you need to constantly “give up” a time slice inside the loop?”

I says you need to give up a time slice so the scheduler can do it’s job. So, agree with it or not, giving up the slice is a necessary evil in Windows threading.

Coleman: the difference is that other threads actually do get CPU time if the badly-behaved thread is at normal priority. You don’t see it because the well-behaved threads have normally woken up, done their work, and gone back to sleep again before their time slice has elapsed, and are therefore using far less than 1% of the CPU time. Process Explorer from www.sysinternals.com can show fractional CPU. It can also show the Context Switch Delta, the number of times this thread was switched to - some threads are doing such tiny amounts of work that they don’t even get credited with any CPU time by the thread time accounting.

If the badly-behaved thread is at high priority and the other runnable threads are at normal priority, at the end of every quantum for the badly-behaved thread, the scheduler will look at the priority queues, see that there’s no other thread at this priority level and just run the badly-behaved thread again. This is what it’s designed to do. It’s only when the balance set manager thread boosts the priority of the starved thread that the starved thread will get to run, and it will only get up to one quantum (about 270ms with XP’s default settings for the foreground application, 90ms for a background application or a special quantum of 4 ticks = 60ms on Windows Server 2003) before its dynamic priority drops back down and the badly-behaved thread gets full control again.

The scheduler is doing its job. Its job is to run higher priority threads until they block. If the threads don’t block, that’s a programming error. Unfortunately developers have a tendency to use all the resources of the system for their task, not considering that it may not be the most important task for the user at that time.

A spinlock is meant to spin for a short time. If it spins for as long as a timer tick (15ms) it’s spinning too long and should have blocked.

A thread doesn’t just block when Sleep or the WaitFor{Single,Multiple}Object{s} family are called. It also blocks when GetMessage is called (PeekMessage does not block and should be avoided where possible). Never use Application.DoEvents - that’s a polling function (it calls PeekMessage) and can cause re-entrancy problems as well. Blocking also occurs whenever disk accesses are needed to satisfy a request, either explicitly using ReadFile (or anything on top of that, e.g. fgets, fscanf, StreamReader), or implicitly when paging code or data from the executable or the page file, or from a memory-mapped file.

If you genuinely have a compute-bound operation whose code and data fits in a small area of memory, then there’s no need to yield as long as you leave the priorities alone - the scheduler will pick another thread at the same priority to run (the one which ran least recently) when your thread’s quantum elapses, if there is a runnable thread. High priorities are for responsiveness where the OS dynamic boosts are not sufficient. I can’t honestly think of a reason to use this.

If you find any operation where you’re polling a value and the API doesn’t offer a blocking version (or a version that signals a synchronisation object), complain to the maker of the API.