Threading Simplified: Part Eleven (Thread Atomicity & Deadlock)

I am again here to continue the discussion around Threading. Today, we will discuss about thread atomicity, deadlock and related concepts. Before that, please read my previous articles on Threading series,

Let’s start by posing some questions to understand the concepts.

What is Atomicity in Multithreading?

If a group of variables or properties are gotten or set within a lock context then they are said to be atomic.

Let’s take the following example,
  1. int num1 = 0;  
  2. int num2 = 0;  
  3. Random rnd = new Random();  
  4. private static objectmyLock = newobject();  
  5.   
  6. public void DoDivide()  
  7. {  
  8.     lock(myLock) //This will ensure that only one thread can enter into code.   
  9.         {  
  10.             for (inti = 0; i < 1000000; i++)  
  11.             {  
  12.                 num1 = rnd.Next(1, 5);  
  13.                 num2 = rnd.Next(1, 5);  
  14.                 Console.WriteLine(num1 / num2);  
  15.                 num1 = 0;  
  16.                 num2 = 0;  
  17.             }  
  18.         }  
  19. }  
Here, num1 and num2 can be said to be atomic as other threads can’t manipulate their values due to enclosed lock.

Does lock always maintains atomicity?

In most cases lock does maintain atomicity, however, there could be some unpredictable issues like system crash or memory exception when the program may exit abnormally in the middle leaving some variables not set properly. This could be a blunder in bank or money related transactions, hence in such cases rollback logic is suggested to be put in to catch or finally block.

What is deadlock and when does it occur?

Deadlock is a situation that generally occurs when more than one thread is waiting for one resource or two threads are waiting for a resource held by each other.

Let’s understand this by a simple example.
  1. static objectlockObj1 = new object();  
  2. static objectlockObj2 = new object();  
  3. static Threadt1 = new Thread(DoWork1);  
  4. static Threadt2 = new Thread(DoWork2);  
  5.   
  6. static void DoWork1()  
  7. {  
  8.     lock(lockObj1)  
  9.     {  
  10.         Console.WriteLine("Inside DoWork1: lockObj1 grabbed");  
  11.         Thread.Sleep(1000);  
  12.         Console.WriteLine("Inside DoWork1, t1 thread state: {0}", t1.ThreadState);  
  13.         Console.WriteLine("Inside DoWork1, t2 thread state: {0}", t2.ThreadState);  
  14.         lock(lockObj2) //Deadlock, below code is not executed  
  15.             {  
  16.                 Console.WriteLine("Inside DoWork1: lockObj2 grabbed");  
  17.             }  
  18.     }  
  19. }  
  20.   
  21. static void DoWork2()  
  22. {  
  23.     lock(lockObj2)  
  24.     {  
  25.         Console.WriteLine("Inside DoWork2: lockObj2 grabbed");  
  26.         Thread.Sleep(500);  
  27.         Console.WriteLine("Inside DoWork2, t1 thread state: {0}", t1.ThreadState);  
  28.         Console.WriteLine("Inside DoWork2, t2 thread state: {0}", t2.ThreadState);  
  29.         lock(lockObj1) ////Deadlock, below code is not executed  
  30.             {  
  31.                 Console.WriteLine("Inside DoWork2: lockObj1 grabbed");  
  32.             }  
  33.     }  
  34. }  
  35.   
  36. static void Main(string[] args)  
  37. {  
  38.     Console.Title = "Thread Deadlock Demo";  
  39.     t1.Start();  
  40.     t2.Start();  
  41. }  
Output:

output

In the output, you can see that statements Inside DoWork1: lockObj2 grabbed and Inside DoWork2: lockObj1 grabbed have not been executed due to deadlock.

You can also sense the deadlock by looking at the thread states (of t1 and t2) in output above.

So, at this point, we understood what deadlock is and when it occurs. Now let’s discuss how to avoid or tackle it.

Steps to avoid deadlock

Before we talk about ways to avoid deadlock, please be informed that deadlock is one of the hardest problems to solve in a multithreaded environment. I am emphasizing this point so that we take steps to avoid deadlock from the initial stages of software development. Now let’s talk about the approaches or best practices that can help us to avoid or minimize it.
  • Follow proven software development design patterns instead of some random design. If you are forced to use some custom built pattern, do the comprehensive testing.

  • Prefer non-blocking constructs and collection (e.g. ConcurrentDictionary) over lock.

  • Prefer having lock object as private variable over class instance.

  • Prefer BeginInvoke over Invoke in Windows Form and WPF applications as BeginInvoke executes asynchronously and hence minimizes the deadlock risk.

  • Where ever possible, prefer data parallelism over task parallelism.

Hope you liked the article. I look forward for your comments/suggestions.

Read more articles on Threading:

X

Build smarter apps with Machine Learning, Bots, Cognitive Services - Start free.

Start Learning Now