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.
Above code work fine in a single threaded program like below:
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;
}
}
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