Multithreading requires careful programming. Threads allow a program to do multiple things concurrently. Since the threads created by a program share the same address space, one thread can modify data that is being used by another thread. This is desirable because it facilitates straightforward communication between threads. But this can lead to undesirable conditions if a improperly written program causes one thread to inadvertently overwrite data being used by another thread.
The sharing of a single address space between multiple threads is one of the reasons that multithreaded programming is usually considered to be more difficult and error prone than programming a single threaded application. Multithreading solves problems with throughput and responsiveness, but in doing so it introduces other potential problems as well, such as deadlocks and race conditions.
Deadlocks
A deadlock occurs when each of two threads tries to lock a resource the other has already locked. Neither thread can make any further progress. With any multithreaded application, there is always a risk of deadlock. A set of processes or threads is said to be deadlocked when each is waiting for an condition that only the other can cause. The simplest case of deadlock is where thread “1” holds an exclusive lock on object “a” and is waiting for a lock on object “b”, while thread “2” holds an exclusive lock on object “b” and is waiting for the lock on object “a”. Unless there is some way to break out of waiting for the lock, the deadlocked threads will wait indefinitely. I have illustrated this condition in following simple example.
The first thread holds a lock on “a” and attempts to get a lock on “b”.
lock(a)
{
lock(b)
{
// code
}
}
The second thread has locked “b” and attempts to get a lock on “a”.
lock(b)
{
lock(a)
{
// code
}
}
In .NET and other programming environments, there are methods in the threading classes that provide timeouts to help the programmer detect deadlocks. The following code attempts to acquire a lock on the object “a”. If the lock is not obtained in 500 milliseconds, Monitor.TryEnter returns false.
if (Monitor.TryEnter(a, 500))
{
try
{
// code protected by the Monitor here.
}
finally
{
Monitor.Exit(a);
}
}
else
{
// if the attempt times out, execute this code.
}
Thread Pool
In the thread pool programming pattern, some number of threads are created to perform some number of tasks, typically organized in a queue. As soon as a thread finishes its task, it will request the next pending task from the queue until all tasks have been completed. The thread can then terminate, or sleep until there are new tasks available.
The advantage of using a Thread Pool over creating a new thread for each task, is that thread creation and destruction overhead is minimized, which may result in enhanced performance and improved system stability.
While the thread pool is a powerful mechanism for structuring multithreaded applications, it also has risks. Applications that use thread pools are subject to the same concurrency issues as any other multithreaded application, for example synchronization errors and deadlocks, and a few other risks specific to thread pools as well, such as pool related deadlocks, thread leakage and resource thrashing.
While deadlock is a risk in any multithreaded program, thread pools introduce one more opportunity for deadlocks, where all pool threads are executing tasks that are blocked waiting for the results of another task on the queue, but the other task cannot run because there is no unused thread available.
Race Conditions
A race condition is a problem that can occur when the result of a program is dependant on which of two or more threads first reaches a particular block of code. Running the program numerous times produces different results, and the result of any given execution cannot be predicted.
A simple example of a race condition is incrementing a field. Suppose a class has a private static field that is incremented every time an instance of the class is created, using code such as “counter++”. This operation requires loading the value from “counter” into a register, incrementing the value, and storing it in “counter”.
In a multithreaded application, a thread that has loaded and incremented the value might be preempted by another thread which performs all three steps; when the first thread resumes execution and stores its value, it overwrites “counter” without taking into consideration that the value has changed in the interim. Race conditions typically occur because the programmer did not anticipate the fact that a thread might be preempted at an problematic moment, sometimes allowing another thread to reach a code block first.