The Lock Statement
As a brief introduction to the lock keyword, it is used as a mechanism to allow access by a thread to a critical piece of code. Typically, this is when you have the possibility of multiple threads trampling on each other's data.Take the sample code here:
using System; using System.Threading; using System.Threading.Tasks; public class Program { public static void Main(string[] args) { // Create a totaliser var totaliser = new Totaliser(); // Set it off on a new thread Task.Run(() => totaliser.ModifyTotal()); // Give the totaliser a chance to do something Thread.Sleep(100); // Write the current total Console.WriteLine(string.Format("Current value of Total: {0}", totaliser.Total)); Console.ReadLine(); } } public class Totaliser { public int Total { get; private set; } // increment the total to 500, then down again to 0 public void ModifyTotal() { for (var counter = 0; counter < 5000000; ++counter) { Total++; } for (var counter = 0; counter < 5000000; ++counter) { Total--; } } }The intent of the Totaliser class is to be able to freely increment and decrement its Total without any external visibility of what's happening. Unfortunately, because its Total property is publicly readable, the Total can be read at any stage in the increment-decrement cycle in a multi-threaded environment:
A solution to this problem is to use the lock statement, around the reads and writes to Total:
using System; using System.Threading; using System.Threading.Tasks; public class Program { public static void Main(string[] args) { // Create a totaliser var totaliser = new Totaliser(); // Set it off on a new thread Task.Run(() => totaliser.ModifyTotal()); // Give the totaliser a chance to do something Thread.Sleep(100); // Write the current total Console.WriteLine(string.Format("Current value of Total: {0}", totaliser.Total)); Console.ReadLine(); } } public class Totaliser { private object lockObject = new object(); private int _total; public int Total { get { lock (lockObject) { return _total; } } private set { _total = value; } } // increment the total to 500, then down again to 0 public void ModifyTotal() { lock (lockObject) { for (var counter = 0; counter < 5000000; ++counter) { Total++; } for (var counter = 0; counter < 5000000; ++counter) { Total--; } } } }See the new lockObject at line 26, and the lock statement at lines 33 and 44.
The lock statement forces the thread to obtain a lock on the lockObject before it can proceed; if another thread has the lock, it must wait until it's been released. The result is a success:
The IL
Looking at the IL produced by the compiler, you can see the framework objects used to implement the lock:.method public hidebysig specialname instance int32
get_Total() cil managed
{
// Code size 48 (0x30)
.maxstack 2
.locals init ([0] bool '<>s__LockTaken0',
[1] int32 CS$1$0000,
[2] object CS$2$0001,
[3] bool CS$4$0002)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
.try
{
IL_0003: ldarg.0
IL_0004: ldfld object Totaliser::lockObject
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: nop
IL_0013: nop
IL_0014: ldarg.0
IL_0015: ldfld int32 Totaliser::_total
IL_001a: stloc.1
IL_001b: leave.s IL_002d
} // end .try
finally
{
IL_001d: ldloc.0
IL_001e: ldc.i4.0
IL_001f: ceq
IL_0021: stloc.3
IL_0022: ldloc.3
IL_0023: brtrue.s IL_002c
IL_0025: ldloc.2
IL_0026: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_002b: nop
IL_002c: endfinally
} // end handler
IL_002d: nop
IL_002e: ldloc.1
IL_002f: ret
} // end of method Totaliser::get_Total
System.Threading.Monitor
Does that mean we could use the System.Threading.Monitor class to manually implement our own lock? Yes, of course. A simple C# lock statement such aslock (lockObject) { DoSomething(); }actually gets compiled as if the C# statements were
object obj = (System.Object)lockObject; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); }The System.Threading.Monitor class has a couple of options when trying to grab a lock on an object, in particular the TryEnter method. This has an overload that takes a timeout value in milliseconds, which specifies how long this thread should wait to obtain the lock
object obj = (System.Object)lockObject; if (System.Threading.Monitor.TryEnter(obj, 3)) { try { //Do something } finally { System.Threading.Monitor.Exit(obj); } } else { //Do something else }I don't advocate rewriting your lock statements to use the longhand versions above, but this hopefully removes a layer of abstraction between you and your multi-threaded executable.
No comments:
Post a Comment