There a lot of things that can go wrong in our code. But when we introduce concurrency or parallelism in our code, we potentially could experience different set of bugs. These are race conditions, deadlocks and data corruption to name a few. This happens because there might be a share piece of data between different thread. The problem created when one thread wants to update a value and another thread might want to access the same value. Thread safety can help us in these situations and reduce a lot of concurrency related bugs.
In this post I’m going to examine what thread safety is and when do we need to be concerned about it. We’re also going to take look at constructs in .Net that are created to mitigate these kind of problems.
What is Thread Safety Means?
What thread safety basically means is that the program function correctly regardless of the number of thread the access that program. Consider this scenario, a method manipulate some data, and this method called from multiple thread. Now the problem is if these thread access the same data, thread one might overwrite the thread two changes and corrupt the data. What can be done in these situations is to put in some mechanism for thread one to wait for thread two to finish. For more through explanation read Eric Lippert post called What is this thing you call “thread safe”?
Thread Safety is a Difficult Subject
There’s no hard and fast rule to make a program thread safe. It’s something that should be determined on subject by subject basis. So as there’s no rule that make a program correct, there’s also no rule to make a program thread safe. Here’s an interesting answer from Eric Lippert about thread safety on Stackoverflow.
So, I guess my ultimate question is: “Is there a short list of rules that define a thread-safe method?
Nope. As you saw from my example earlier an empty method can be non-thread-safe. You might as well ask “is there a short list of rules that ensures a method is correct”. No, there is not. Thread safety is nothing more than an extremely complicated kind of correctness. Moreover, the fact that you are asking the question indicates your fundamental misunderstanding about thread safety. Thread safety is a global, not a local property of a program. The reason why it is so hard to get right is because you must have a complete knowledge of the threading behavior of the entire program in order to ensure its safety.
When We Need To Watch out For Thread Safety?
In order to answer this question, we need to understand what’s the source of most thread safety issues? The answer is shared state, that is we share some kind of data between threads. If we eliminate that issue, we came a long way to attenuating problems regarding thread safety. So basically we need to think about this problem when we use a share variable or state in or program.
But it’s not that simple, because if we decide to not share any state we have to copy it. What it means is that we potentially need to copy large amount of data for each thread and this can have a detrimental effect on our performance. So what’s the solution? In next section I’ll go through some possible solutions, some of them is very simple and doesn’t involve any extra tools or programming constructs. But some need to use the mechanism that created to address these kind of issues.
How Can we Make a Program Thread Safe
There’s a lot of ways to achieve thread safety. Some of them involve using internal construct of for example C# language. Others are more related to strategy and structure of our program. I examine both of those in a high level overview.
One way to reduce the number of thread safety issue is to use local state as opposed to global states when ever possible. The important thing is, every thread has its own independent stack. When we create a variable in that thread, this variable place on this stack. This means other threads can’t access the value of these variables. But that still doesn’t guarantee thread safety, read The no-lock deadlock by Eric Lippert.
If no one change the state of our program, then it doesn’t matter if the data is share between different threads.
Mutual Exclusion and Atomic operations
This means only one thread should be able to access the resource at any point in time. For example the
lock statement around a statement is one example of this in .Net. Atomic operations are almost similar and achieve the same thing either by using Mutexes or Semaphores which use atomic operations internally.
Different Tools And Example Of Making a Program Thread Safe
In this section I’ll go through some examples that illustrate the different approaches to make a program thread safe.
consider this example, this class contains a method which is not thread safe. The reason is it shares a field between different threads.
The bad thing that can happen here is, on line 15 we read the value of
shareState field. But before that we assign the value of
shareState with parameter1. What can happen here is another thread could enter the method and change the value of
shareState. Now when the previous thread tries to read the field, the value is not value it suppose to be. In other word, one thread think the field has some value but in fact it doesn’t. Because another thread entered the method and change field, and since the field is shared between the two, the program is not accurate. But the fix the easy.
As you can see in above code excerpt, here we don’t share any state between threads. We removed the
shareState filed and instead we create a variable locally. Finally instead of mutating the value of the shareState, we simply return the value. Note that we could also use lock, but it was not necessary because we could change the structure of our code to fix that.
It’s important to note that if there is not a lot of contention (read collision or fight for resource), another thread is not going to wait for Monitor, so the overhead of using lock is negligible. Also despite the above contrived example, most of the time fixing race condition without synchronization can make our code more complicated.
Another approach that can help in thread safety is immutability. Notice that I said it can help but it’s not a silver bullet, because you can always change the reference of an object. Even though it’s immutable, so the reference of an object is not immutable. Consider this object.
If we use this object in our concurrent program, it is ensured that the value stays the same. With immutability we can ensure the consistency of the data and somewhat simplify the thread safety of our program. Because we have one less state to worry about. But there’s always a trade off because here we create a new object every time we need to add something to value. So we should have a cost/benefit analysis before going with this approach.
Mutual Exclusion and Atomic operations
There are a lot of construct in C# language that help us write thread safe programs. But I introduce three of them here.
Interlocked class support three operation: Increment, Decrement, and Add (for subtract Add a negative number). These methods turn non-atomic operations into atomic. Here’s an example.
Worth to note that this can slow down the program, in fact the thread safe version of this program is 4 to 10 time slower then the non thread safe one.
Monitor and Lock
Consider this code example.
ReceivePayment is not thread safe, and we what to make it thread safe, so I created
ReceivePaymentThreadSafe that uses a Monitor to make the method thread safe. There’s a construct in CLR called sync block index. I’m oversimplifying but the Monitor class use this index to for its internal mechanism to lock states. The Monitor class has a static method called
Enter, enter tries to take control of the current block and doesn’t allow other threads to enter. After the thread that has the ownership of the sync block done its job, we release the block by calling Exit.
There’s another method in the code above called
ReceivePaymentWithLock. The lock keyword do the same thing as Monitor it’s a syntactic sugar. Basically lock is just shortcut for
Monitor.Exit. The lock keyword is created so we wouldn’t have to worry about using the Monitor properly. For example if we lock and we get an exception,the object is going to be in a locked state forever, so we better use Monitor like this with finally block, like what lock statement does for us.
Asynchronous Lock with SemaphoreSlim
Unlike Monitor and lock semaphores does not enforce mutual exclusion. That is they do not ristrict access to the resource to only one thread. Instead it allows access to specified number of thread which can be passed into its constructor. Consider this piece of code. Here we can’t use lock in
RefreshAmountAsync method, because lock only works in synchronous context.
In these situations we can use
SemaphoreSlim. So I change the above code to this.
Here we created a semaphore and passed the number of threads that we want to allow inside the block. Internally, SemaphoreSlim uses the Wait and Pulse methods of the Monitor class. These two method help with communication between threads. One thread wait for signal and another thread pulse a signal. So SemaphoreSlim is very light weight construct that helps us to protect any kind of resource.
Imagine you have a method that has a very fast read, but the write is slow. Now if you use lock, your program cannot serve the threads that only want to read. They have to wait to for write to finish for them to be able to read. But with
ReaderWriterLockSlim, we can lock some part of code for write, but we can allow read for other threads. So because read is fast and write is not, or reader can crack on while other part of our code is busy reading. Here’s an example form MSDN.
Note that for a fast read and write, the ReaderWriterLockSlim is not suitable and is slower than normal lock. It’s useful when there’s a mismatch between the speed of read and write. So it’s imperative that we allow one of them whole locking another.
In this post we saw what thread safety is and when do we need to be concerned about thread safety of our application. We also saw different tools and programming constructs in C# that can help us reduce a possibility of bugs in asynchronous programs.