Beware of Deadlocks

 < Day Day Up > 



Though essential for thread safety, synchronization, if not used properly, can cause deadlocks. As such, it is very important to understand what deadlocks are and how to avoid them. Deadlocks occur when two or more threads are waiting for two or more locks to be freed and the circumstances in the program logic are such that the locks will never be freed. Figure 3 illustrates a typical deadlock scenario.

click to expand
Figure 3

In the figure, Thread 1 acquires lock L1 on an object by entering its critical section. In this critical section, Thread 1 is supposed to acquire lock L2. Thread 2 acquires lock L2 and is supposed to acquire lock L1. So, now Thread 1 cannot acquire lock L2 because Thread 2 owns it and Thread 2 cannot acquire lock L1 because Thread 1 owns it. As a result, both the threads enter into an infinite wait or deadlock.

One of the best ways to prevent the potential for deadlock is to avoid acquiring more than one lock at a time, which is often practicable. However, if that is not possible, you need a strategy that ensures you acquire multiple locks in a consistent, defined order. Depending on each program design, the synchronization strategies to avoid deadlocks may vary. There is no standard strategy that can be applied to avoid all deadlocks. Most of the time, deadlocks are not detected until the application is deployed on a full-scale basis. We can consider ourselves lucky if we are able to detect deadlocks in our program during the testing phase.

A critical, but often overlooked element of any locking strategy is documentation. Unfortunately, even in cases where a good synchronization strategy is designed to avoid deadlocks, much less effort is made in documenting it. At the minimum, every method should have documentation associated with it that specifies the locks that it acquires and describes the critical sections within that method.

Let's take a look at an example, Deadlock.cs:

    using System;    using System.Threading;    namespace DeadLock    {      class DL      {        int field 1 = 0;        private object lock 1 = new int[1];        int field_2 = 0;        private object lock 2 = new int[1];        public void First(int val)        {          lock(lock 1)           {             Console.WriteLine("First:Acquired lock 1:" +                               Thread.CurrentThread.GetHashCode() +                               " Now Sleeping");             //Try commenting Thread.Sleep()             Thread.Sleep(1000);             Console.WriteLine("First:Acquired lock 1:" +                               Thread.CurrentThread.GetHashCode() +                               " Now wants lock_2");             lock(lock 2)             {               Console.WriteLine("First:Acquired lock 2:" +                                 Thread.CurrentThread.GetHashCode());               field 1 = val;               field 2 = val;             }           }         }         public void Second(int val)         {           lock(lock 2)           {             Console.WriteLine("Second:Acquired lock 2:" +                               Thread.CurrentThread.GetHashCode());             lock(lock_1)            {              Console.WriteLine("Second:Acquired lock_1:" +                                Thread.CurrentThread.GetHashCode()); field 1 = val;              field_2 = val;            }          }        }      }      public class MainApp      {        DL d = new DL();        public static void Main()        {           MainApp m = new MainApp();           Thread t1 = new Thread(new ThreadStart(m.Run1));           t1.Start();           Thread t2 = new Thread(new ThreadStart(m.Run2));           t2.Start();        }        public void Run1()        {          this.d.First(10);        }        public void Run2()        {          this.d.Second(10);        }      }    } 

The output from DeadLock is:

    First:Acquired lock_1:2 Now Sleeping    Second:Acquired lock_2:3    First:Acquired lock_1:2 Now wants lock_2 

In DeadLock, thread t1 calls the First() method, acquires lock_1, and goes to sleep for one second. In the meantime, thread t2 calls the Second() method and acquires lock_2. Then it tries to acquire lock_1 in the same method. But lock_1 is owned by thread t1, so thread t2 has to wait until thread t1 releases lock_1. When thread t1 wakes up, it tries to acquire lock_2. Now lock_2 is owned by thread t2 and thread t1 cannot acquire it until thread t2 releases lock_2. This results in a deadlock and a hung program. Commenting out the Thread.Sleep() line from the method First() does not result in deadlock, at least temporarily, because, thread t1 acquires lock_2 before thread t2. But, in real-world scenarios, instead of Thread.Sleep(), we might connect to a database resulting in thread t2 acquiring lock_2 before thread t1, and it will result in a deadlock. The example shows how important it is to carve out a good locking scheme in any multithreaded application. A good locking scheme may incorporate the acquisition of lock by all the threads in a well defined manner. In the case of the example above, thread t2 should not acquire lock_2 until it is release by thread t2 or thread t2 should not acquire lock_1 until thread t1 releases it. These decisions depend on specific application scenarios and cannot be generalized in any way. Testing of the locking scheme is equally important, because deadlocks usually occur in deployed systems due to lack of stress and functional testing.



 < Day Day Up > 



C# Threading Handbook
C# Threading Handbook
ISBN: 1861008295
EAN: 2147483647
Year: 2003
Pages: 74

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net