When I started ASP.NET and web programming I thought that I was shielded from race conditions that I learned about in college. Wrong! I found out the hard way after trying to track down an intermittent bug that it is a very real problem even in web programming and that includes ASP.NET. This becomes an issue when you use static variables or methods sometimes. Most of the time I have not found there to be issues with race conditions. However, sometimes there is. This is not the only time it is an issue though. Beware.
I found that it can be difficult to consistently reproduce the bug in web code, though I think it can be done. To make things easier, I simulated the web. I created two threads to create the race condition. I didn't use a Threading Pool, but I think this is close enough, and illustrates the point while reliably and consistently reproducing the race condition.
First I guess I should explain a little bit about what a race condition is. Let's assume you have two threads. They could be two web sessions as each connection to the your ASP.NET application is handled by a thread from the thread pool. The issues comes when those two threads try to access any shared resource. Thread-A sets a value for example, has some kind of delay, and while Thread-A is processing / delaying Thead-B comes along and changes the shared resource before Thread-A has a chance to use / retrieve value from the shared resource. Then when Thread-A finally has a chance to access the shared resource without coding for thread-safety, Thread-A will assume it was the only one that had used the shared resource and thus it will get data it was not expecting. It is probably a good idea to always only allow one object access to a shared resource and only allow one calling object at a time actually call the method.
I think the easiest way to see this is to look at the output of a program I wrote to see what was actually going on.
Thread-A-Starting
Thread-A-Spawned
Thread-A-Entering Critical Section
Thread-A-New
Generated Value is: 1907360620
Set New Value: 1907360620
Thread-A-Waiting.... forcing race condition
Thread-B-Starting
Thread-B-Spawned
Thread-B-Entering Critical Section
Thread-B-New
Generated Value is: 209463673
Set New Value: 209463673
Thread-B-Waiting.... forcing race condition
Get New Value: 209463673
Thread-A-CriticalSection() returns: 209463673 (Important Line *******)
Thread-A-Exiting Critical Section
Get New Value: 209463673
Thread-B-CriticalSection() returns: 209463673 (Important Line *******)
Thread-B-Exiting Critical Section
The important thing to note is that the return value for both threads is the last value that was set, and this is because both threads entered the critical section of code at the same time or near the same time. When this happens, they basically step on each other's feet and you get bad results.
Below is the same output but there is a lock on the critical section of code so that only one thread can enter it at a time.
Thread-A-Starting
Thread-A-Spawned
Thread-A-Entering Critical Section
Thread-A-New Generated Value is: 1483178371
Set New Value: 1483178371
Thread-A-Waiting.... forcing race condition
Thread-B-Starting
Thread-B-Spawned <Press any key to remember>
Get New Value: 1483178371
Thread-A-CriticalSection() returns: 1483178371 (Important Line *******)
Thread-A-Exiting Critical Section
Thread-B-Entering Critical Section
Thread-B-New Generated Value is: 249049990
Set New Value: 249049990
Thread-B-Waiting.... forcing race condition
Get New Value: 249049990
Thread-B-CriticalSection() returns: 249049990 (Important Line *******)
Thread-B-Exiting Critical Section
The important thing to notice is that the values set by each thread are also retrieved by each respective thread. This is what we want to happen. You will also notice that even though the threads start together or close together, they take turns using the shared resource. In fact, one thread uses the shared resource until it is done with it, and while the other thread waits to use it.
In C# there is something called a Monitor class that locks and unlocks shared resources. They have simplified the syntax much in the same way that they have with database connectivity. If you are familiar with the using () block in your code. You know it implicitly handles the closing a connection for example (even if there is an exception). Well, the lock () block does the same thing for the Monitor class. It basically guarantees that the object will be unlocked even if there is an exception. It does NOT handle deadlocks, though you can do so with try-catch-finally and the Monitor class because it has a TryEnter and allows you to specify how long to wait for a lock before giving up (thus breaking a deadlock).
Below is a snippet of how locks work.
private static Object objLock = new Object();
public void CriticalSection()
{
lock (objLock)
{
// use shared resource here
}
}
Things to note here are that the objLock variable is both static (this extremely important) and it is also private so that no one else can unlock the lock (which would defeat its purpose). You can use different lock variables for locking different parts of your code. The smaller number of lines of code you lock at a time the better your performance will be. So, you may want to create different locks for different methods if they are not accessing the same shared resource.
If you would like a much more in-depth and more technical explanation of the how and why I suggest what I suggest I recommend you check
I found that it can be difficult to consistently reproduce the bug in web code, though I think it can be done. To make things easier, I simulated the web. I created two threads to create the race condition. I didn't use a Threading Pool, but I think this is close enough, and illustrates the point while reliably and consistently reproducing the race condition.
First I guess I should explain a little bit about what a race condition is. Let's assume you have two threads. They could be two web sessions as each connection to the your ASP.NET application is handled by a thread from the thread pool. The issues comes when those two threads try to access any shared resource. Thread-A sets a value for example, has some kind of delay, and while Thread-A is processing / delaying Thead-B comes along and changes the shared resource before Thread-A has a chance to use / retrieve value from the shared resource. Then when Thread-A finally has a chance to access the shared resource without coding for thread-safety, Thread-A will assume it was the only one that had used the shared resource and thus it will get data it was not expecting. It is probably a good idea to always only allow one object access to a shared resource and only allow one calling object at a time actually call the method.
I think the easiest way to see this is to look at the output of a program I wrote to see what was actually going on.
Thread-A-Starting
Thread-A-Spawned
Thread-A-Entering Critical Section
Thread-A-New
Generated Value is: 1907360620
Set New Value: 1907360620
Thread-A-Waiting.... forcing race condition
Thread-B-Starting
Thread-B-Spawned
Thread-B-Entering Critical Section
Thread-B-New
Generated Value is: 209463673
Set New Value: 209463673
Thread-B-Waiting.... forcing race condition
Get New Value: 209463673
Thread-A-CriticalSection() returns: 209463673 (Important Line *******)
Thread-A-Exiting Critical Section
Get New Value: 209463673
Thread-B-CriticalSection() returns: 209463673 (Important Line *******)
Thread-B-Exiting Critical Section
The important thing to note is that the return value for both threads is the last value that was set, and this is because both threads entered the critical section of code at the same time or near the same time. When this happens, they basically step on each other's feet and you get bad results.
Below is the same output but there is a lock on the critical section of code so that only one thread can enter it at a time.
Thread-A-Starting
Thread-A-Spawned
Thread-A-Entering Critical Section
Thread-A-New Generated Value is: 1483178371
Set New Value: 1483178371
Thread-A-Waiting.... forcing race condition
Thread-B-Starting
Thread-B-Spawned <Press any key to remember>
Get New Value: 1483178371
Thread-A-CriticalSection() returns: 1483178371 (Important Line *******)
Thread-A-Exiting Critical Section
Thread-B-Entering Critical Section
Thread-B-New Generated Value is: 249049990
Set New Value: 249049990
Thread-B-Waiting.... forcing race condition
Get New Value: 249049990
Thread-B-CriticalSection() returns: 249049990 (Important Line *******)
Thread-B-Exiting Critical Section
The important thing to notice is that the values set by each thread are also retrieved by each respective thread. This is what we want to happen. You will also notice that even though the threads start together or close together, they take turns using the shared resource. In fact, one thread uses the shared resource until it is done with it, and while the other thread waits to use it.
In C# there is something called a Monitor class that locks and unlocks shared resources. They have simplified the syntax much in the same way that they have with database connectivity. If you are familiar with the using () block in your code. You know it implicitly handles the closing a connection for example (even if there is an exception). Well, the lock () block does the same thing for the Monitor class. It basically guarantees that the object will be unlocked even if there is an exception. It does NOT handle deadlocks, though you can do so with try-catch-finally and the Monitor class because it has a TryEnter and allows you to specify how long to wait for a lock before giving up (thus breaking a deadlock).
Below is a snippet of how locks work.
private static Object objLock = new Object();
public void CriticalSection()
{
lock (objLock)
{
// use shared resource here
}
}
Things to note here are that the objLock variable is both static (this extremely important) and it is also private so that no one else can unlock the lock (which would defeat its purpose). You can use different lock variables for locking different parts of your code. The smaller number of lines of code you lock at a time the better your performance will be. So, you may want to create different locks for different methods if they are not accessing the same shared resource.
If you would like a much more in-depth and more technical explanation of the how and why I suggest what I suggest I recommend you check
2 comments:
Good helpful Article. Thanks
Sameer,
Thank you for the feedback. I'm so glad you found it helpful.
Thank you,
Brent
Post a Comment