Sunday, July 13, 2014

C# - Lock & Monitor

Today topic is about going deep into the lock syntax. When we write a multithreaded program and there are multiple threads trying to access and update an object, the common problem that we may encounter is the result is out of sync.

For instance, if you are required to create a simple program that generates sequence number, and the number must not duplicate, how would you code it?

The following is the implementation for a simple sequence number generator.

public class Sequence
{
    public static int CurrentNumber { get; set; }

    public int Next()
    {
        int result = 0;

        result = ++CurrentNumber;

        return result;
    }

}

Above code work fine in a single threaded program like below:

class Program
{
    static void Main(string[] args)
    {
        Sequence seq = new Sequence();

        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Next number: " + seq.Next());
        }

        Console.ReadKey();
    }
}

But, ever wonder what will happen if run the above code in a multithreaded program? Instead of writing a lot of code to spawn threads to test it out, I will just use Parallel class library is good enough. See the following modified program:

class Program
{
    static void Main(string[] args)
    {
        Sequence seq = new Sequence();

        Parallel.For(0, 100, i =>
        {
            Console.WriteLine("Next number: " + seq.Next());
        });

        Console.ReadKey();
    }
}

And, here is the result:


Duplicate numbers were generated, and it is wrong. So, how to make it right? When there are many threads trying to access one object at the same time, we have to make all the threads to wait except one is allowed to run.

The code block which required threads to wait is called critical section. In order to prevent all the threads entering into the critical section, we have to use the lock syntax (which actually uses Monitor class) to lock the critical section and only allowing one thread accessing the code block at one time.

The code block that generates the sequence number is the place where need to apply the lock. So, how to use the lock syntax? Check this out:

public class Sequence
{
    public static int CurrentNumber { get; set; }
    private static object _lock;

    static Sequence()
    {
        _lock = new object();
    }

    public int Next()
    {
        int result = 0;

        lock (_lock)
        {
            result = ++CurrentNumber;
        }
           
        return result;
    }
}

Also, take note that when using the lock syntax, avoid locking a public modifier object because once it is locked, no one can access it, not even its member. It will cause unnecessary wait to other threads which uses that object. Therefore, it is bad to use the lock in the following ways:

lock (this)

The this is the public Sequence class, it will cause other threads to wait which just want to access the CurrentNumber property.

lock (typeof(Sequence))

Locking typeof(Sequence) will cause wait to other threads that trying to use the Sequence type.

The best practice is to instantiate a private object for lock.

This is how the Intermediate Language (IL) look like when using the lock syntax:

.method public hidebysig instance int32  Next() cil managed
{
  // Code size       46 (0x2e)
  .maxstack  2
  .locals init ([0] int32 result,
           [1] bool '<>s__LockTaken0',
           [2] object CS$2$0000)
  IL_0000:  ldc.i4.0
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.1
  .try
  {
    IL_0004:  ldsfld     object LockSample.Sequence::_lock
    IL_0009:  dup
    IL_000a:  stloc.2
    IL_000b:  ldloca.s   '<>s__LockTaken0'
    IL_000d:  call       void [mscorlib]System.Threading.Monitor::Enter(object,
                                                                        bool&)
    IL_0012:  call       int32 LockSample.Sequence::get_CurrentNumber()
    IL_0017:  ldc.i4.1
    IL_0018:  add
    IL_0019:  dup
    IL_001a:  call       void LockSample.Sequence::set_CurrentNumber(int32)
    IL_001f:  stloc.0
    IL_0020:  leave.s    IL_002c
  }  // end .try
  finally
  {
    IL_0022:  ldloc.1
    IL_0023:  brfalse.s  IL_002b
    IL_0025:  ldloc.2
    IL_0026:  call       void [mscorlib]System.Threading.Monitor::Exit(object)
    IL_002b:  endfinally
  }  // end handler
  IL_002c:  ldloc.0
  IL_002d:  ret
} // end of method Sequence::Next

Note the yellow highlighted IL code? Therefore, the lock syntax is translated into:

public class Sequence
{
    public static int CurrentNumber { get; set; }
    private static object _lock;

    static Sequence()
    {
        _lock = new object();
    }

    public int Next()
    {
        int result = 0;

        try
        {
            Monitor.Enter(_lock);
            result = ++CurrentNumber;
        }
        finally
        {
            Monitor.Exit(_lock);
        }

        return result;
    }
}

The lock syntax only uses the Enter method and Exit method to mark the beginning and ending of critical section and acquiring and releasing lock which are good enough to control the thread access. You may use other available methods in the Monitor class library for more extensive thread control such as the Wait method to temporary release current thread lock and wait for signal to acquire lock, or the Pulse method to send signals to all waiting threads that the state of the lock object has changed, the queuing threads can ready to acquire the lock.

I would like to keep this post simple, this simple sequence number generator does not require a complicated thread handling, no point in using Wait and Pulse methods. However, if you want to know more about using these 2 methods, checkout the MSDN arcticles for Wait and Pulse. And, warn you, if you are trying to make the threads interact with each other by using Wait and Pulse, it is very likely would cause deadlock because Monitor class does not keep track a Pulse method has been called or not, if one thread has called Wait after a thread called Pulse, that waiting thread will wait forever.

Enjoy locking~


No comments:

Post a Comment

Send Transactional SMS with API

This post cover how to send transactional SMS using the Alibaba Cloud Short Message Service API. Transactional SMS usually come with One Tim...