11.6 C
New York
Thursday, February 27, 2025

Understanding thread synchronization in C#




lock (sharedObj1)
{
   ...
   lock (sharedObj2)
   {
       ...
   }
}

Note that the order of the locks in the Thread2Work method has been changed to match the order in Thread1Work. First a lock is acquired on sharedObj1, then a lock is acquired on sharedObj2.

Here is the revised version of the complete code listing:


class DeadlockDemo
{
    private static readonly object sharedObj1 = new();
    private static readonly object sharedObj2 = new();
    public static void Execute()
    {
        Thread thread1 = new Thread(Thread1Work);
        Thread thread2 = new Thread(Thread2Work);
        thread1.Start();
        thread2.Start();
        thread1.Join();
        thread2.Join();
        Console.WriteLine("Finished execution.");
    }
    static void Thread1Work()
    {
        lock (sharedObj1)
        {
            Console.WriteLine("Thread 1 has acquired a shared resource 1. " +
                "It is now waiting for acquiring a lock on resource 2");
            Thread.Sleep(1000);
            lock (sharedObj2)
            {
                Console.WriteLine("Thread 1 acquired a lock on resource 2.");
            }
        }
    }
    static void Thread2Work()
    {
        lock (sharedObj1)
        {
            Console.WriteLine("Thread 2 has acquired a shared resource 2. " +
                "It is now waiting for acquiring a lock on resource 1");
            Thread.Sleep(1000);
            lock (sharedObj2)
            {
                Console.WriteLine("Thread 2 acquired a lock on resource 1.");
            }
        }
    }
}

Refer to the original and revised code listings. In the original listing, threads Thread1Work and Thread2Work immediately acquire locks on sharedObj1 and sharedObj2, respectively. Then Thread1Work is suspended until Thread2Work releases sharedObj2. Similarly, Thread2Work is suspended until Thread1Work releases sharedObj1. Because the two threads acquire locks on the two shared objects in opposite order, the result is a circular dependency and hence a deadlock.

In the revised listing, the two threads acquire locks on the two shared objects in the same order, thereby ensuring that there is no possibility of a circular dependency. Hence, the revised code listing shows how you can resolve any deadlock situation in your application by ensuring that all threads acquire locks in a consistent order.

Best practices for thread synchronization

While it is often necessary to synchronize access to shared resources in an application, you must use thread synchronization with care. By following Microsoft’s best practices you can avoid deadlocks when working with thread synchronization. Here are some things to keep in mind:

  • When using the lock keyword, or the System.Threading.Lock object in C# 13, use an object of a private or protected reference type to identify the shared resource. The object used to identify a shared resource can be any arbitrary class instance.
  • Avoid using immutable types in your lock statements. For example, locking on string objects could cause deadlocks due to interning (because interned strings are essentially global).
  • Avoid using a lock on an object that is publicly accessible.
  • Avoid using statements like lock(this) to implement synchronization. If the this object is publicly accessible, deadlocks could result.

Note that you can use immutable types to enforce thread safety without needing to write code that uses the lock keyword. Another way to achieve thread safety is by using local variables to confine your mutable data to a single thread. Local variables and objects are always confined to one thread. In other words, because shared data is the root cause of race conditions, you can eliminate race conditions by confining your mutable data. However, confinement defeats the purpose of multi-threading, so will be useful only in certain circumstances.


Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles