Managed threading best practices
Multithreading requires careful programming. For most tasks, you can reduce complexity by queuing requests for execution by thread pool threads. This topic addresses more difficult situations, such as coordinating the work of multiple threads, or handling threads that block.
Note
Starting with .NET Framework 4, the Task Parallel Library and PLINQ provide APIs that reduce some of the complexity and risks of multi-threaded programming. For more information, see Parallel Programming in .NET.
Deadlocks and race conditions
Multithreading solves problems with throughput and responsiveness, but in doing so it introduces new problems: 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.
Many methods of the managed threading classes provide time-outs to help you detect deadlocks. For example, the following code attempts to acquire a lock on an object named lockObject
. If the lock is not obtained in 300 milliseconds, Monitor.TryEnter returns false
.
If Monitor.TryEnter(lockObject, 300) Then
Try
' Place code protected by the Monitor here.
Finally
Monitor.Exit(lockObject)
End Try
Else
' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(lockObject, 300)) {
try {
// Place code protected by the Monitor here.
}
finally {
Monitor.Exit(lockObject);
}
}
else {
// Code to execute if the attempt times out.
}
Race conditions
A race condition is a bug that occurs when the outcome of a program depends on which of two or more threads reaches a particular block of code first. Running the program many times produces different results, and the result of any given run cannot be predicted.
A simple example of a race condition is incrementing a field. Suppose a class has a private static field (Shared in Visual Basic) that is incremented every time an instance of the class is created, using code such as objCt++;
(C#) or objCt += 1
(Visual Basic). This operation requires loading the value from objCt
into a register, incrementing the value, and storing it in objCt
.
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 objCt
without taking into account the fact that the value has changed in the interim.
This particular race condition is easily avoided by using methods of the Interlocked class, such as Interlocked.Increment. To read about other techniques for synchronizing data among multiple threads, see Synchronizing Data for Multithreading.
Race conditions can also occur when you synchronize the activities of multiple threads. Whenever you write a line of code, you must consider what might happen if a thread were preempted before executing the line (or before any of the individual machine instructions that make up the line), and another thread overtook it.
Static members and static constructors
A class is not initialized until its class constructor (static
constructor in C#, Shared Sub New
in Visual Basic) has finished running. To prevent the execution of code on a type that is not initialized, the common language runtime blocks all calls from other threads to static
members of the class (Shared
members in Visual Basic) until the class constructor has finished running.
For example, if a class constructor starts a new thread, and the thread procedure calls a static
member of the class, the new thread blocks until the class constructor completes.
This applies to any type that can have a static
constructor.
Number of processors
Whether there are multiple processors or only one processor available on a system can influence multithreaded architecture. For more information, see Number of Processors.
Use the Environment.ProcessorCount property to determine the number of processors available at run time.
General recommendations
Consider the following guidelines when using multiple threads:
Don't use Thread.Abort to terminate other threads. Calling
Abort
on another thread is akin to throwing an exception on that thread, without knowing what point that thread has reached in its processing.Don't use Thread.Suspend and Thread.Resume to synchronize the activities of multiple threads. Do use Mutex, ManualResetEvent, AutoResetEvent, and Monitor.
Don't control the execution of worker threads from your main program (using events, for example). Instead, design your program so that worker threads are responsible for waiting until work is available, executing it, and notifying other parts of your program when finished. If your worker threads do not block, consider using thread pool threads. Monitor.PulseAll is useful in situations where worker threads block.
Don't use types as lock objects. That is, avoid code such as
lock(typeof(X))
in C# orSyncLock(GetType(X))
in Visual Basic, or the use of Monitor.Enter with Type objects. For a given type, there is only one instance of System.Type per application domain. If the type you take a lock on is public, code other than your own can take locks on it, leading to deadlocks. For additional issues, see Reliability Best Practices.Use caution when locking on instances, for example
lock(this)
in C# orSyncLock(Me)
in Visual Basic. If other code in your application, external to the type, takes a lock on the object, deadlocks could occur.Do ensure that a thread that has entered a monitor always leaves that monitor, even if an exception occurs while the thread is in the monitor. The C# lock statement and the Visual Basic SyncLock statement provide this behavior automatically, employing a finally block to ensure that Monitor.Exit is called. If you cannot ensure that Exit will be called, consider changing your design to use Mutex. A mutex is automatically released when the thread that currently owns it terminates.
Do use multiple threads for tasks that require different resources, and avoid assigning multiple threads to a single resource. For example, any task involving I/O benefits from having its own thread, because that thread will block during I/O operations and thus allow other threads to execute. User input is another resource that benefits from a dedicated thread. On a single-processor computer, a task that involves intensive computation coexists with user input and with tasks that involve I/O, but multiple computation-intensive tasks contend with each other.
Consider using methods of the Interlocked class for simple state changes, instead of using the
lock
statement (SyncLock
in Visual Basic). Thelock
statement is a good general-purpose tool, but the Interlocked class provides better performance for updates that must be atomic. Internally, it executes a single lock prefix if there is no contention. In code reviews, watch for code like that shown in the following examples. In the first example, a state variable is incremented:SyncLock lockObject myField += 1 End SyncLock
lock(lockObject) { myField++; }
You can improve performance by using the Increment method instead of the
lock
statement, as follows:System.Threading.Interlocked.Increment(myField)
System.Threading.Interlocked.Increment(myField);
Note
Use the Add method for atomic increments larger than 1.
In the second example, a reference type variable is updated only if it is a null reference (
Nothing
in Visual Basic).If x Is Nothing Then SyncLock lockObject If x Is Nothing Then x = y End If End SyncLock End If
if (x == null) { lock (lockObject) { x ??= y; } }
Performance can be improved by using the CompareExchange method instead, as follows:
System.Threading.Interlocked.CompareExchange(x, y, Nothing)
System.Threading.Interlocked.CompareExchange(ref x, y, null);
Note
The CompareExchange<T>(T, T, T) method overload provides a type-safe alternative for reference types.
Recommendations for class libraries
Consider the following guidelines when designing class libraries for multithreading:
Avoid the need for synchronization, if possible. This is especially true for heavily used code. For example, an algorithm might be adjusted to tolerate a race condition rather than eliminate it. Unnecessary synchronization decreases performance and creates the possibility of deadlocks and race conditions.
Make static data (
Shared
in Visual Basic) thread safe by default.Do not make instance data thread safe by default. Adding locks to create thread-safe code decreases performance, increases lock contention, and creates the possibility for deadlocks to occur. In common application models, only one thread at a time executes user code, which minimizes the need for thread safety. For this reason, the .NET class libraries are not thread safe by default.
Avoid providing static methods that alter static state. In common server scenarios, static state is shared across requests, which means multiple threads can execute that code at the same time. This opens up the possibility of threading bugs. Consider using a design pattern that encapsulates data into instances that are not shared across requests. Furthermore, if static data are synchronized, calls between static methods that alter state can result in deadlocks or redundant synchronization, adversely affecting performance.
See also
Feedback
https://aka.ms/ContentUserFeedback.
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see:Submit and view feedback for