Locks, threads and this (huh?)
In this article we will explore a commonly misunderstood and misused keyword in C#: lock
Let’s say you have block of code being accessed by multiple threads. This piece of code needs to be protected in such a way that you allow only one thread to access the block at any given point of time. This is known as mutual exclusion and the block of code you are trying to protect is called the critical section. I won’t go into details of thread synchronization here. There are lots of articles out there that explain it in great detail.
One way of achieving this in C# is by using the lock() keyword. The lock keyword marks a block of code as a critical section by obtaining mutual exclusion lock on an object.
Lock takes one parameter, an expression which specifies the object to lock on to. The expression should be a reference type.
Syntax:
lock(expression)
{
//Critical section
}
We will have a look at what happens if you don’t protect your code from simultaneous thread access.
Take a look at the method below:
Listing 1:
void DoSomeJob()
{
for (int i=0 ; i <10;++i)
{
//Start critical section
Console.WriteLine(”\r\n ”
+ System.Threading.Thread.CurrentThread.Name
+ ” is STARTING Transaction ” + i.ToString());
System.Threading.Thread.Sleep(300);
Console.WriteLine(”\r\n ”
+ System.Threading.Thread.CurrentThread.Name
+ ” is ENDING Transaction ” + i.ToString());
//End critical section
}
}
The DoSomeJob method prints a line to console, then sleeps for 300 ms and then prints the second line. It does this 10 times. Pretty simple, isn’t it. Now let’s get TWO threads to execute the same method simultaneously and see what happens.
Let’s add the creation of the two threads on the click handler of a button named uxStartThreadsButton.
Listing 2:
private void uxStartThreadsButton_Click
(object sender, System.EventArgs e)
{
Thread thread1 = new Thread(new ThreadStart(DoSomeJob));
thread1.Name = “Thread 1″;
Thread thread2 = new Thread(new ThreadStart(DoSomeJob));
thread2.Name = “Thread 2″;
thread1.Start();
thread2.Start();
}
When you click on the button, two threads names thread1 and thread2 are created and started. The output of this operation is shown below:
Listing 3:
Thread 1 is STARTING Transaction 0
Thread 2 is STARTING Transaction 0
Thread 2 is ENDING Transaction 0
Thread 2 is STARTING Transaction 1
Thread 1 is ENDING Transaction 0
Thread 1 is STARTING Transaction 1
Thread 1 is ENDING Transaction 1
Thread 1 is STARTING Transaction 2
Thread 2 is ENDING Transaction 1
Thread 2 is STARTING Transaction 2
Thread 2 is ENDING Transaction 2
Thread 2 is STARTING Transaction 3
Thread 1 is ENDING Transaction 2
Thread 1 is STARTING Transaction 3
(rest of the output has been truncated for clarity)
Uh, not exactly what we expected eh? The expectation was that thread A starting operation X would complete the same operation before allowing other operations to be started. The sleep statement has been inserted to force a context switch so that we can easily make out what is happening. This is a trivial case, consider what would have happened if we had a bank transaction. One thread would start a transaction to deduct cash from an account which would have resulted in zero balance. Before the change could be reflected, another thread starts a similar operation to deduct some more cash. Ideally this transaction should be rejected because the balance is zero, but the system doesn’t realize it and proceeds to deduct the funds. Then the first thread resumes operation and commits changes and the bank balance turns negative!
This requirement is known as atomicity of operations. Atomicity dictates that a set of instructions have to executes sequentially without interruption and HAS to run to completion before allowing other operations to operate on the same data. In thread synchronization terms, the critical section should not be entered by any other thread while one thread is accessing it.
This is where lock comes in. Let’s execute the same program, but this time add the lock statement as shown.
Listing 4:
void DoSomeJob()
{
for (int i=0;i<10;++i)
{
lock(lockObject)
{
//Start critical section
Console.WriteLine(”\r\n ”
+ System.Threading.Thread.CurrentThread.Name
+ ” is STARTING Transaction ” + i.ToString());
System.Threading.Thread.Sleep(300);
Console.WriteLine(”\r\n ”
+ System.Threading.Thread.CurrentThread.Name
+ ” is ENDING Transaction ” + i.ToString());
//End critical section
}
}
}
Listing 5:
The output below is as expected:
Thread 1 is STARTING Transaction 0
Thread 1 is ENDING Transaction 0
Thread 2 is STARTING Transaction 0
Thread 2 is ENDING Transaction 0
Thread 1 is STARTING Transaction 1
Thread 1 is ENDING Transaction 1
Thread 2 is STARTING Transaction 1
Thread 2 is ENDING Transaction 1
Thread 1 is STARTING Transaction 2
Thread 1 is ENDING Transaction 2
Thread 2 is STARTING Transaction 2
Thread 2 is ENDING Transaction 2
Thread 1 is STARTING Transaction 3
Thread 1 is ENDING Transaction 3
So what did lock do? Well, thread 1 entered the for loop first and printed the first line and then went off to sleep. Seizing this opportunity, thread 2 begins execution and enters the for loop, but alas, it finds that thread 1 has obtained a mutually exclusive lock on lockObject before it entered the critical section. (I will explain what lockObject is in just a bit) This means that thread 2 will have to WAIT till thread 1 exits the block. By this time (after 300ms), thread 1 resumes, prints the second line, exits the block and releases the lock on lockObject. Thread 2 is can now obtain lock on the lockObject and enter the critical section.
The biggest problem I found with people who use lock is this. No really, it is literally this. Most people use lock as shown below:
Listing 6:
void DoSomeJob()
{
for (int i=0 ; i<10;++i){
lock(this)
{
//Start critical section
//Code here
//End critical section
}
}
}
A lot of people think that this is the best way to use lock. In fact it is the worst. You really don’t need to lock the whole object you are in to obtain mutual exclusion. This also means that two running threads that need to operate on two different critical sections which don’t share any common objects are unable to run concurrently because they are locked by the same reference object, this. I would blame Microsoft partially for this. When most people started learning C#, they, like most windows developers, referred MSDN and in the help page for lock for .net 1.1 found the following explanation:
lock(expression)
expression
“Specifies the object that you want to lock on. expression must be a reference type.
Typically, expression will either be this, if you want to protect an instance variable, or typeof(class), if you want to protect a static variable.”
But Microsoft did an about turn for .net 2.0 and pointed this out as a big no no.
Another reason why I recommend not locking on this is because you might be passing the same reference object to several other classes, all of which can potentially use it to lock on to, thereby leading to a deadlock. And trust me you don’t want to be debugging threading related deadlocks. I have lost half my sanity this way.
Of course, a genuine case might arise which would require you to guard two critical sections using the same lock to really ensure sequential access, as shown in listing 4. So if using this is a bad practice, what is the suggested method?
Well my recommendation is to declare a private object for each critical section to be protected:
private Object _lockObject = new Object();
This was the lockObject I showed you in listing4. Note that this will protect critical section only for that instance of the class. What does that mean? Well, Consider the code below. Lets assume that DoSomeWork() is a now a public method of class MyClass and myObj1 and myObj2 are objects of that class.
Listing 7:
private void uxStartThreadsButton_Click
(object sender, System.EventArgs e)
{
Thread thread1 = new Thread(new ThreadStart(myObj1.DoSomeJob));
thread1.Name = “Thread 1″;
Thread thread2 = new Thread(new ThreadStart(myObj2.DoSomeJob));
thread2.Name = “Thread 2″;
thread1.Start();
thread2.Start();
}
Note that the above code will create two threads that execute on two separate instances on DoSomeJob(). Since the lock is instance specific (each thread will lock on to the object’s corresponding private object) thread 1 and thread 2 will execute in parallel. This might be alright is most cases as the data inside the critical section we are trying to protect will be instance specific. On the other hand, if both the methods are of the same instance, they would take turns is accessing the critical section as shown in listing 4.
And lastly, to lock critical sections across all instances, use a private static object
private static Object _lockObject = new Object();
For completeness sake I’ll mention two other expressions that can be used with lock. I highly recommend that you don’t use them because of the pitfalls mentioned.
1. lock(typeOf(type)): typeOf() returns a System.Type of the type you pass. Using lock this way is not recommended if the type is public. Same problem as this.
eg: lock(typeOf(MyClass))
2. lock(”somestring“): This is a classic. Using lock this way is also not recommended because any other code using the same string will share the lock!
Well, that’s it for this edition. Hopefully you are better off that when you started reading this article. If not, the blame is all mine. I should have done a better job. But if you promise to keep reading and commenting, I promise to keep improving.
In the next edition I shall be covering problems and best practices on accessing UI controls from non UI threads, invokes and a real life deadlock problem that happens when you put them all together using masking tape.
Code Safely.
Alex