Thread Locking

Exclusive locking in threading ensures that one thread does not enter a critical section while another thread is in the critical section of code. If another thread attempts to enter a locked code, it will wait (block) until the object is released. To achieve this functionality we have to main exclusive locking constructs are

  • Mutex
  • Lock

The lock construct is faster and more convenient. Mutex, though, has a place in that its lock can span applications in different processes on the computer.

The lock statement takes the following form:

lock(expression) statement_block

Where

expression : Specifies the object that you want to lock on. expression must be a reference type.
statement_block :The statements of the critical section. A critical section is a piece of code that accesses a shared resource (data structure or device) but the condition is that only one thread can enter in this section in a time.

In the following example we implement threads and lock. As long as the lock statement is present, the statement block is a critical section and Salary will never become a negative number.

using System;
using System.Threading;
using System.Diagnostics;
namespace Threadlocking
{
class
Department
    {
        private Object thisLock = new Object();
        int salary;
        Random r = new Random();
        public Department(int initial)
        {
            salary = initial;
        }
        int Withdraw(int amount)
        {
            // This condition never is true unless the lock statement
            // is commented out.
            if (salary < 0)
            {
                throw new Exception("Negative Balance");
            }
            // Comment out the next line to see the effect of leaving out
            // the lock keyword.
            lock (thisLock)
            {
                if (salary >= amount)
                {
                    Console.WriteLine("salary before Withdrawal :  " + salary);
                    Console.WriteLine("Amount to Withdraw        : -" + amount);
                    salary = salary - amount;
                    Console.WriteLine("salary after Withdrawal  :  " + salary);
                    return amount;
                }
                else
                {
                    return 0; // transaction rejected
                }
            }
        }
        public void DoTransactions()
        {
            for (int i = 0; i < 100; i++)
            {

                Withdraw(r.Next(1, 100));
            }
        }
    }
class Process
    {
        static void Main()
        {
            Thread[] threads = new Thread[10];
            Department dep = new Department(1000); 
           for (int i = 0; i < 10; i++)
            {
               Thread t = new Thread(new ThreadStart(dep.DoTransactions));
                threads[i] = t;
            }
            for (int i = 0; i < 10; i++)
            {
                threads[i].Start();
            }
            Console.Read();
        }
    }
}

Output :

lock.gif

In general, we avoid locking because lock (this) is a problem if the instance can be accessed publicly.

Nested Locking

A thread can repeatedly lock the same object in a nested (reentrant) fashion:

lock (locker)
lock (locker)
lock (locker)
{
// Do something...
}

In the nested locking, the object is unlocked only when the outermost lock statement has exited. It is useful when one method calls another within a lock:

using System;
using System.Threading;
using System.Diagnostics;
namespace nestedlocking
{
class Program
    {
        static object x = new object();
        static void AnotherMethod()
        {
            lock (x) { Console.WriteLine("Another method"); }
        }
        static void Main()
        {
            lock (x)
            {
                AnotherMethod();
                // We still have the lock - because locks are reentrant.
            }
            Console.Read();
        }
    }
}

Output:

nestloc.gif

The problem with threading

Lock-based resource protection and thread/process synchronization have many disadvantages:

  • They cause blocking, which means some threads/processes have to wait until a lock (or a whole set of locks) is released.

  • Lock handling adds overhead for each access to a resource, even when the chances for collision are very rare. (However, any chance for such collisions is a race condition).

  • Lock contention limits scalability and adds complexity.

  • Priority Inversion High priority threads/processes cannot proceed, if a low priority thread/process is holding the common lock.

  • Convoying. All other threads have to wait, if a thread holding a lock is descheduled due to a time-slice interrupt or page fault.

  • Hard to debug: Bugs associated with locks are time dependent. They are extremely hard to replicate.

Next Recommended Readings