Tuesday, September 13, 2011

Deadlocks in .Net


Deadlocks

Before starting to use the thread pool in your applications you should know one additional concept: deadlocks. A bad implementation of asynchronous functions executed on the pool can make your entire application hang.
Imagine a method in your code that needs to connect via socket with a Web server. A possible implementation is opening the connection asynchronously with the Socket class'BeginConnect method and wait for the connection to be established with the EndConnect method. The code will be as follows:

class ConnectionSocket { public void Connect() { IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName()); IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0], 80); Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null); s.EndConnect(ar); } }

So far, so good—calling BeginConnect makes the asynchronous operation execute on the thread pool and EndConnect blocks waiting for the connection to be established.
What happens if we use this class from a function executed on the thread pool? Imagine that the size of the pool is just two threads and we launch two asynchronous functions that use our connection class. With both functions executing on the pool, there is no room for additional requests until the functions are finished. The problem is that these functions call our class' Connect method. This method launches again an asynchronous operation on the thread pool, but since the pool is full, the request is queued waiting any thread to be free. Unfortunately, this will never happen because the functions that are using the pool are waiting for the queued functions to finish. The conclusion: our application is blocked.
We can extrapolate this behavior for a pool of 25 threads. If 25 functions are waiting for an asynchronous operation to be finished, the situation becomes the same and the deadlock occurs again.
In the following fragment of code we have included a call to the last class to reproduce the problem:


class MainApp
{
   static void Main()
   {
      for(int i=0;i<30;i++)
      {
         ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc));
      }
      Console.ReadLine();
   }

   static void PoolFunc(object state)
   {
      int workerThreads,completionPortThreads;
      ThreadPool.GetAvailableThreads(out workerThreads,
         out completionPortThreads);
      Console.WriteLine("WorkerThreads: {0}, CompletionPortThreads: {1}", 
         workerThreads, completionPortThreads);

      Thread.Sleep(15000);
      ConnectionSocket connection = new ConnectionSocket();
      connection.Connect();
   }
}

If you run the example, you see how the threads on the pool are decreasing until the available threads reach 0 and the application stops working. We have a deadlock.
In general, a deadlock can appear whenever a pool thread waits for an asynchronous function to finish. If we change the code so that we use the synchronous version of Connect, the problem will disappear:


class ConnectionSocket
{
   public void Connect()
   {
      IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName());
      IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0], 80);
      Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream,
         ProtocolType.Tcp);
      s.Connect(ipEndPoint);
   }
}

If you want to avoid deadlocks in your applications, do not ever block a thread executed on the pool that is waiting for another function on the pool. This seems to be easy, but keep in mind that this rule implies two more:
  • Do not create any class whose synchronous methods wait for asynchronous functions, since this class could be called from a thread on the pool.
  • Do not use any class inside an asynchronous function if the class blocks waiting for asynchronous functions.
If you want to detect a deadlock in your application, check the available number of threads on the thread pool when your system is hung. The lack of available threads and CPU utilization near 0% are clear symptoms of a deadlock. You should monitor your code to identify where a function executed on the pool is waiting for an asynchronous operation and remove it.

No comments:

Post a Comment