Processors today have reached the limits of sheer speed and manufacturers have instead resorted to increasing the number of cores in processors to continue to advance computing power. This is great, but applications must now be written specifically to take advantage of multiple processors by becoming multi threaded. Thankfully, modern frameworks such as .NET make this easy to manage.
The primary issue with multi threading is how to share resources between threads without them interfering with one another. Resources in this case can be file, network connections or even in memory variables. In a single threaded application, we do not need to worry about these issues as only one line of code will be executing at any one time but in a multi threaded application, there may be many. This can cause some new issues that we need to take into consideration when developing multi threaded applications.
Race Conditions
One of the most likely issues you would face when creating a multi threaded application is a race condition. This occurs when one thread is writing to a variable at the same time another thread is reading from it. The result of doing so is undefined. You might read the correct value without issue, or you might read half of the new value being written and half of the old one. This leads to data corruption and unexpected behaviour in your application.
The main objective when making a variable shared between threads “thread safe” is to ensure that only one thread at a time can access it. There are two main ways of doing this, using atomic operations or using some kind of mutual exclusion mechanism.
Atomic Operations
Atomic operations are operations that are carried out in a single step and are guaranteed not to be interrupted mid operation. In the case of a shared variable being written to by one thread and read by another at the same time, the write would be done in a single step. This would make it impossible for the other thread to read half and half of the old and new values. Atomic operations are usually simple operations such as swapping, incrementing etc.
In the .NET framework, these operations are found in the System.Threading.Interlocked class. It contains static methods for adding, decrementing incrementing etc. One of the most useful methods is the “CompareExchange” method. This allows you to check if two objects are equal, and if so, swap the value to a new object. You could use this to check if an object is null and if so, set it to a new value in a single, uninterrupted operation.
Although atomic operations are fast an easy to use, they are limited by their simplicity. Atomic operations can only perform simple operations such as increment, decrement swap etc. More complex operations that rely upon multiple shared resources require the use of other methods.
The Lock and SyncLock Keywords
One of the greatest aids to multithreading in the .NET framework are the built-in Lock (C#) and SyncLock(VB.NET) keywords. These keywords accept a single object as a parameter. The object can only have a single lock on it at any one time. This provides safety when it comes to multithreading as it ensures that if multiple threads try to place a lock on the same object at the same time, only one will succeed. If another thread does try to place a lock on an already “locked” object, it will be blocked. This means that the thread will cease execution until the object becomes unlocked and it is able to obtain a lock itself.
These keywords are used to surround a block of code that needs to be executed by a single thread at a time. This is also known as a critical section.
Example:
Object o;
lock(o)
{
Console.WriteLine(o.ToString());
}
In the example above, the object “o” is used as the lock object. Only a single thread at a time will be able to access the “WriteLine” piece of code.
One issue with locks however is that they can degrade performance over using atomic operations, the simple reason being that threads sometimes need to wait to access code within a critical section. The locking process as well also involved some additional overhead to manage the lock. This shouldn’t dissuade you from using locks however as often, they can be the only way to achieve true thread safety.
Deadlock
One issue introduced by locking is known as dead lock. Say we have two resources, A and B. If thread 1 comes along and locks A and thread 2 locks object B. So far so good, each thread successfully obtains its lock. Deadlock occurs however if Thread 1 attempts to lock resource B while it still holds A and thread 2 attempts to lock resource A while it still holds B. Both threads are waiting on each other to unlock a resource and will both wait forever. This is deadlock.
One way to reduce the chance of these deadlocks is to have threads only ever hold one lock at a time. This cannot always be avoided however. Another helpful tip is to always lock objects in the same order, so in this example, always locking A first then B would prevent the deadlock occurring.
Concurrent Collections
The .NET framework contains many more thread synchronisation tools but I want to give special mention to a new addition to the .NET 4 framework that can save a lot of trouble when sharing data across threads, the new concurrent collections. These new collections wrap all the thread safety code such as atomic operations and locks within the collection classes themselves so the developer does not need to worry about it. These collections can be found in the System.Collections.Concurrent namespace. Using these collections in single threaded applications should be avoided though as some of them, such as the ConcurrentDictionary use locking to achieve thread safety which does incur a small overhead.
Conclusion
Threading can dramatically improve application performance by taking advantage of more of its CPU resources. It’s also often necessary in applications with user interfaces as waiting for blocking operations like network I/O will freeze the interface if run on the same thread. Care must always be taken with resources that are shared between threads for issues like deadlock and race conditions however. Good stuff!