C#’s lock statement provides built-in language support for synchronizing multi-thread access to blocks of code. Under the hood, lock is syntactical sugar simplifying the use of .Net’s Monitor exclusive lock. Let’s examine three ways Monitor can be used: via lock, directly and via a method attribute.
lock
Access to a block of code commonly known as a critical section can be restricted to a single thread at a time via lock.
class Example { private object lockObject = new object(); public void DoSomething() { lock (lockObject) { // When one thread is executing this code block using // a particular lock object instance, all other threads // attempting to enter the code block using the same // lock object instance will be force to wait until // the first thread exits the block and then will be // allowed to enter the block one at a time.. } } }
This syntax should be familiar to anyone who’s coded thread synchronization in C# before.
Directly
A lock code block is a syntax shortcut for placing the block inside a try…finally statement proceeded with a call to Monitor.Enter and concluded with a call to Monitor.Exit in the finally block. The end result varies slightly based on the CLR version but will be something along the lines of:
public void DoSomething() { Monitor.Enter(lockObject); try { // Code to execute goes here } finally { Monitor.Exit(lockObject); } }
The beauty of using Monitor directly is not the more verbose syntax but rather the advanced functionality provided by the class. A lock statement blocks until the thread is obtains the lock; with Monitor, a thread can TryEnter which fails immediately or after a timeout if the lock cannot be obtained, allowing your code to execute alternate logic when the lock is unavailable. Monitor also allows a lock-possessing thread to release its lock and place itself in a waiting queue where it will sit until another thread signals it to move to the ready queue so that it may resume execution when it receives the lock back.
As lock is syntactical sugar built on top of Monitor, calls to Monitor methods can be made from inside lock blocks.
public void DoSomething() { lock (lockObject) { // Code Monitor.Wait(lockObject); // More code } }
Attribute
If an entire method is a critical section, applying a MethodImplAttribute with the MethodImplOptions value of Synchronized wraps the entire method in a lock.
[MethodImpl(MethodImplOptions.Synchronized)] public void DoSomething() { // Code }
Watch out! If the method is an instance method, the current object instance is used as the locking object; if the method is static, the class type object is used. If the lock object used is publicly accessible, code elsewhere could also lock using the same object The potential result? Unnecessary and unexpected deadlocking your code. A best practice (see also) is to only lock on a private object or a private static object variable. This attribute-based approach, if used at all, must be used carefully to prevent inadvertent violation of this best practice.