Most of the time, when we’re programming, we can think about our programs in a very sequential fashion: First do this, then do that, then finish with the other. This is even true with modern, multicore architectures, including the PC, XBox 360, and PlayStation 3. Usually, it’s for one of two reasons: Either there is no concurrency in our system, or the concurrency has been abstracted out so that we don’t have to think about the hard problems associated with it.
Obviously, it’s desirable to be in the second camp, and there are lots of ways to get there. The problem is that, in order to get there properly, somebody has to write the fancy-dancy-multi-threaded-code. Go ahead, guess what I’m working on right now?
Call me “Somebody.”
Writing thread-safe code is hard. Writing efficient thread-safe code is extra hard. Writing efficient, lockless, thread-safe code is mind-bogglingly hard: the sort of thing that will send people to the loony bin if they’re not careful. Fortunately, with a lot of patience, a lot of diligence, and a good source of information, you can avoid the loony bin.
In my current project, I’ve had to use:
- A simple critical section lock
- A multi-reader-single-writer lock
- InterlockedExchangePointer
- InterlockedCompareExchangePointer
- InterlockedCompareExchange
All of those are for one data structure. (Well, actually a set of two data structures designed to work together, but you get the picture.) In this series of posts, I’m going to be talking about multithreaded programming, and giving tips that I’ve learned for writing efficient thread-safe code using the locks and lock-free structures I just listed.
I’ll be using the Win32 API functions for all of the code in this series, but there exist equivalents in pretty much any other system that you’d care to use. Now, on to the good stuff: the Critical Section Lock.
A critical section is a lightweight type of mutual exclusion lock. There are many types of mutual exclusion locks, including one which is generally called a mutex. The mutex lock is typically a heavier-weight lock, which can control access to a shared resource across processes. Critical sections are designed to control access to a shared resource within a single process.
This “shared resource” I’m referring to can be anything; it’s defined by your program. So, let’s say that I have the following code:
SharedResource gRes;
void FrobSharedResource()
{
Frob(gRes);
}
This is fine code, but it falls apart as soon as you have more than one thread trying to Frob() the resource.
Which brings us to my first tip for multithreaded programming. This tip (which I learned from Raymond Chen) is applicable to far more than just multithreaded programming, but it’s particularly helpful here, so here it is:
Tip #1: What if two threads did this?
In our example, Frob() is a void function, so it’s probably transforming the shared resource in some way. If you had two threads both Frobbing the resource at the same time, your code would end up in a strange, inconsistent state. Don’t believe me? What if our Frob function looked like this:
void Frob(SharedResource& res)
{
if(res.spoo->fleem > res.fleem_limit)
{
delete res.spoo;
res.spoo = NULL;
}
}
Now, imagine the following scenario:
- Thread A enters Frob and sees that the spoo has too much fleem
- Thread A gets interrupted by the OS
- Thread B gains control
- Thread B enters Frob and sees that the spoo has too much fleem
- Thread B gets interrupted by the OS
- Thread A gains control
- Thread A deletes the spoo
- Thread A gets interrupted by the OS
- Thread B gains control
- Thread B deletes the spoo
- CRASH! The spoo has been double-deleted
And that’s in a function with three lines of code. Imagine the debugging when you have some real code with hundreds of lines. Which brings me to the second tip:
Tip #2: Your threads can be interrupted at any time. Yes, even then. Yes, then too. No really, any time means any time.
With very few exceptions, your code can be interrupted at any time for any length of time. There are any number of things that could interrupt your thread: the scheduling quantum could expire, a device interrupt could go off, a timer ticks… Basically, your computer is full of entropy. It’s entirely possible that your thread will wake up, execute exactly one processor instruction, and then get interrupted.
“Yeah,” you may say, “but what are the odds of that happening in my code? It’s gotta be, like, one in a million!”
This brings up the third tip:
Tip #3: One in a million is next Tuesday.
You need to deal with these situations. If your code is executed a million times in a week, then one in a million is next Tuesday. Somebody is going to run into that exact sequence, and your program is going to crash, and you’ll have nobody to blame but yourself.
Fix your threading bugs before they happen. If you see it, fix it. If you don’t see any problems, go looking for them, because chances are they exist. Don’t depend on chance to save your butt, because it won’t. I approach every threaded data structure that I have to debug as though every line of code contains at least one threading bug.
So, we’ve seen all of the problems that this little chunk of code can cause. Let’s see what we can do to fix it.
Let’s take a closer look at the critical section. In Win32, we would do the following:
SharedResource gRes;
CRITICAL_SECTION g_csResourceLock;
void InitSharedResourceLock()
{
InitializeCriticalSection(&g_csResourceLock);
}
void ShutdownSharedResourceLock()
{
DeleteCriticalSection(&g_csResourceLock);
}
void FrobSharedResource()
{
EnterCriticalSection(&g_csResourceLock);
Frob(gRes);
LeaveCriticalSection(&g_csResourceLock);
}
We add some new functions to initialize and clean up the critical section object. The magic happens with the EnterCriticalSection() and LeaveCriticalSection() functions. The Enter function will acquire an exclusive lock, so that no other thread can access the lock at the same time. If another thread tries to access the lock, then it will be blocked until the first thread leaves the critical section. It’s a simple synchronization primitive that is lightweight and very effective.
Let’s try to break this code by running through a “what if two threads did this” problem:
- Thread A enters FrobSharedResource and enters the critical section
- Thread A gets interrupted, Thread B gains control
- Thread B enters FrobSharedresource() and attempts to enter the critical section, but it’s already owned by Thread A, so it blocks, Thread A gains control
- Thread A frobs the resource
- Thread A gets interrupted, but Thread B is blocked waiting for the critical section, so Thread A gains control again
- Thread A leaves the critical section
- Thread A gets interrupted, Thread B gains control
- Thread B now has the critical section
- Thread B frobs the resource
- Thread B leaves the critical section
That worked well. Let’s try again:
- Thread A enters FrobSharedResource, but gets interrupted before it can enter the critical section
- Thread B gains control
- Thread B enters FrobSharedResource, and enters the critical section
- Thread B gets interrupted, Thread A gains control
- Thread A attempts to enter the critical section, but it’s already owned by Thread B, so it blocks, Thread B gains control
- From here, execution is basically the same as before, except that Threads A and B are reversed.
It works! With two lines of code, we’ve eliminated our threading issues.
Next time, we’ll look at some situations where a critical section isn’t quite good enough for our needs.
Go to Part 2.