In a previous article, I introduced you to the denizens of the Microsoft .NET Framework's System.Threading namespace, which allows you to build free-threaded applications with ease. I also touched on thread synchronization, a problem inherent to multithreaded applications.
The synchronization issue lies in the fact that multiple threads in a process share access to the same memory space and, therefore, the same globally visible variables. On the surface, this doesn't seem like a big deal, but consider what happens if the system suspends a thread before it has finished operating on a shared variable and then activates a second thread that will access that same variable. Let's look at some code to make this problem a little easier to understand.
I never said math was my strong point
In Listing A, you'll find some C# code that provides a good, if rather contrived, example of what can happen when a task switch is made. ThreadSyncApp consists of two classes, Worker and the accurately named NotThreadSafe. Looking at Main, you'll see that it creates two threads and calls the same instance of Worker.Go on each of them. Worker.Go then calls NotThreadSafe.AddTwoNumbers to sum two counter variables it increments inside its for loop. Notice that AddTwoNumbers is a void method, so to get the result of the call, a client must use the NotThreadSafe.Result property to obtain the sum of the two numbers. I'll explain why in a moment.
To make things interesting, I simulated a task switch by putting the current thread to sleep immediately after each call to AddTwoNumbers on every loop iteration except the first. The results, shown in Figure A, are fairly predictable.
|Hmm… I've got to show this to my accountant.|
If you'll hold your cries of "No wonder it doesn't work" for just a few paragraphs longer, I'll explain why I purposely did things this way.
The wonderful world of MSIL
In .NET, the code you write is not directly interpreted at runtime, but neither is it compiled into native/machine code. Like Java's VM, the Common Language Runtime (CLR) executes a kind of byte code called Microsoft Intermediate Language (MSIL). Now, the simple act of adding two variables and storing the result in a shared variable, like this fragment taken from Listing A:
result = Number1 + Number2
is, at the MSIL level anyway, at least four separate instructions.
IL_0004: stfld int32 ThreadDemo.NotThreadSafe::result
Even though MSIL looks a bit like Klingon, it's pretty easy to intuit what's going on there:
- · Load a variable into a register.
- · Load a second variable into another register.
- · Sum the contents of the registers.
- · Store the result into the member variable result.
A real Type A personality
Let me ask you a question: What happens if thread A, which is executing these operations, gets suspended between steps 3 and 4? Can that happen? Yes, it can. In fact, because task switches are performed at the assembler level, and the ratio of assembler statements to MSIL byte code statements isn't always exactly one-to-one, a task switch could actually occur inside an MSIL statement!
Ordinarily, a task switch that occurs during an operation isn't a problem; the system allows for that by preserving the contents of the CPU registers (referred to as the context) as they were before thread A was suspended. A switch is problematic only if the task that the system switches to is another thread running in the same process (a context switch), thread B, which happens to execute the same section of code that thread A was executing before it was suspended.
What happens then? If the variable that is to receive the sum is shared between threads A and B, and multiple context switches occur with the right timing (and the moon is full), thread A could wind up with the results of thread B's computation, or vice versa.
Now, I could spend paragraphs explaining how this could happen, but I'd likely lose you halfway through my discussion and run well over my word limit. So I'll instead hope that a picture is indeed worth a thousand words and refer you to Figure B, which illustrates the process.
|Okay, so maybe the moon doesn't have anything to do with it.|
You can see this scenario play out in my example code in Listing A. Worker.Go suspends one thread after it has called AddTwoNumbers but before it can retrieve the result through Result. That leaves the second thread free to add two different numbers before the first thread resumes. When the first thread does resume, it gets the second thread's result instead of its own. This winds up making 2+3=7 or some similar nonsense.
Although I've concentrated on variables here, synchronization problems aren't limited to them. The use of any shared resource can be problematic if a context switch occurs before a thread has finished using it.
Truth in journalism
I'll be honest: The chances of what I've described actually happening, while dependent upon several factors, are usually pretty small—on the order of once in millions of executions. As a case in point, I spent roughly 10 hours attempting to concoct an application to use as an example in this article that would fail due to an unforced synchronization problem on a consistent basis. As you can see from the example code I wound up using, I wasn't able to come up with one.
However, don't take this to mean that you should not be concerned with synchronization in your multithreaded apps. How in the world are you going to correct a bug that only evidences itself once in a few million runs? Sure, that's a small chance of encountering a bug, but imagine trying to track down this error if you've gotten a report from a user (your boss, perhaps?) who was unlucky enough to hit it five times in a row. If you can't make it break, how are you going to fix it? The relatively small threat of a synchronization bug also becomes a big problem if you are dealing with an application that demands 100 percent availability and accuracy.
The ability to create a free-threaded application is a double-edged sword. Your first line of defense should be a sound design. However, even then, you'll likely still be vulnerable to the scenario I introduced you to in this article or one of thousands like it. In those situations, you'll turn to the synchronization tools that .NET makes available. These tools will be the subject of the next article in this series.
A stitch in time
What tips or advice do you have for someone wanting to develop a multithreaded application? Send us an e-mail with your suggestions and experiences.