This topic has not yet been rated - Rate this topic

Thread-Safe Components

Sharing resources between threads is a frequent necessity in multithreaded programming. Multiple threads may need to access a shared database, for instance, or make updates to a set of system variables. When more than one thread simultaneously competes for access to shared resources, the possibility of a race condition occurs. A race condition exists when a thread modifies a resource to an invalid state, and then another thread attempts to access that resource and use it in the invalid state. Consider the following example:

public class WidgetManipulator
{
   public int TotalWidgets = 0;
   public void AddWidget()
   {
      TotalWidgets++;
      Console.WriteLine("Total widgets = " + TotalWidgets.ToString());
   }
   public void RemoveWidgets()
   {
      TotalWidgets -= 10;
   }
}

This class exposes two methods. One method, AddWidget, adds 1 to the TotalWidgets field and writes the value to the console. The second method subtracts 10 from the value of TotalWidgets. Consider what would happen if two threads simultaneously attempted to access the same instance of the WidgetManipulator class. One thread might call AddWidget at the same time that the second thread called RemoveWidgets. In that case, the value of TotalWidgets could be changed by the second thread before an accurate value could be reported by the first thread. This race condition can cause inaccurate results to be reported and can cause corruption of data.

You can protect critical sections of your code from race conditions by employing locks. A lock, represented by the Visual Basic keyword SyncLock Statement, or the C# keyword lock Statement, allows a single thread of execution to obtain exclusive execution rights on an object. The following example demonstrates locks:

lock(MyObject)
{
   // Insert code that affects MyObject.
}

When a lock is encountered, execution on the object specified (MyObject in the previous example) is blocked until the thread can gain exclusive access to the object. When the end of the lock is reached, the lock is freed and execution proceeds normally. You can only obtain a lock on an object that returns a reference. A value type cannot be locked in this fashion.

Although using locks will guarantee that multiple threads do not simultaneously access an object, they can cause significant performance degradation. Imagine a program with many different threads running. If each thread needs to use a particular object and has to wait to obtain an exclusive lock on that object before executing, the threads will all cease executing and back up behind one another, causing poor performance. For these reasons, you should only use locks when you have code that must be executed as a unit. For example, you might update multiple resources that were interdependent. Such code is said to be atomic. Restricting your locks only to code that must be executed atomically will allow you to write multithreaded components that ensure the safety of your data while still maintaining good performance.

You must also be careful to avoid situations where deadlocks might occur. In this case, multiple threads wait for each other to release shared resources. For example, Thread 1 might hold a lock on resource A and is waiting for resource B. Thread 2, on the other hand, might have a lock on resource B and awaits resource A. In such a case, neither thread will be allowed to proceed. The only way to avoid deadlock situations is through careful programming.

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.