MethodImplOptions.Synchronized
Recent discussions with colleagues regarding use of MethodImplOptions.Synchronized prompted me to discuss its implications.
Let’s begin with formal documentation. The MethodImplOptions.Synchronized option “specifies that the method can be executed by only one thread at a time. Static methods lock on the type, while instance methods lock on the instance. Only one thread can execute in any of the instance functions and only one thread can execute in any of a class’s static functions”.
I’d like to focus on the statement “static methods lock on the type”. Microsoft strongly discourages locking on any public types, or on instances you do not control. This means the common constructs lock(this), lock(typeof(SomeType)), and lock(“myLock”) violate this guideline. If you’re not yet familiar with the reason behind this guideline then please read Joe Duffy’s blog entry titled “Rude unloads and orphaned locks“. To provide additional support for Joe’s comments, the formal documentation for MethodImplOptions states “Locking on the instance or on the type, as with the Synchronized flag, is not recommended for public types because code other than your own can take locks on public types and instances. This might cause deadlocks or other synchronization problems.”
To be explicit, the lock(typeof(MyClass)) semantic is exactly what MethodImplOptions.Synchronized implements when adorned on static methods . The lock in this case is across all AppDomains in the same operating system process. Consider the following code, which if executed from multiple AppDomains would result in a conflict.
ManualResetEvent e1= new ManualResetEvent(false);
lock (typeof(object)) {
AppDomain domain = AppDomain.CreateDomain("MyDomain");
domain.DoCallBack(delegate {
ThreadPool.QueueUserWorkItem(delegate {
ManualResetEvent e2= new ManualResetEvent(false);
lock (typeof(object)) {
e2.Set();
}
});
});
e1.WaitOne();
}
As a result, you should shy away from using MethodImplOptions.Synchronized for the same reasons you wouldn’t use lock(this) or lock(typeof(SomeClass)).
Additionally, the MethodImplOptions.Synchronized option favors neither readers nor writers. Use of a low-locking technique such as a spinlock or the intrinsic ReaderWriterLockSlim and Monitor classes will perform and scale much better on both single-core and multi-core (CPU) hardware.
The above points are important for several reasons:
- The scope of a lock is very relevant if a goal of your code is reuse. As a framework writer is it your responsibility to ensure consumers writing applications with differing scalability thresholds and concurrency rates are equally favored; the solution should either work for all consumers or offer options for each category of consumer.
- If you’re writing code that is to be used in systems where efficient use of system resources (CPU and memory) is paramount then it is your responsibility to provide an implementation that meets this fundamental goal.
- If you’re executing under the Microsoft SQL Server CLR host and your code takes too long to execute then SQL will force an AppDomain unload, which means (as a specific example), that a finally block wishing to execute Monitor.Exit won’t even be run. As Joe indicates, until you’ve created 4,294,967,295 threads such that the Thread IDs wrap around and the old ID gets assigned to a new thread, and that thread spuriously decides to Exit the Monitor without acquiring it first, your system is going to be locked up for a bit. In other words, deadlocked.
- The MethodImplAttribute can only be used to adorn (you guessed it) methods.
So how much of a difference can a low-locking technique really make? That obviously depends on the technique. I wrote a parallelized program (using the Microsoft Parallel Extensions for .NET 3.5) that tests synchronization using MethodImplOptions.Synchronized, Monitor, Interlocked, ReaderWriterLockSlim, and a spin-lock. For those interested, I’ve included my spin-lock implementation to the bottom of this article.
Result Set 1: Write-only access:
- Monitor executed 10,000,000 iterations in 13,372,540 (ticks), which is 747 iterations per tick.
- Interlocked executed 10,000,000 iterations in 18,767,746 (ticks), which is 532 iterations per tick.
- ReaderWriterLockSlim executed 10,000,000 iterations in 18,908,171 (ticks), which is 528 iterations per tick.
- MethodImpl executed 10,000,000 iterations in 22,724,176 (ticks), which is 440 iterations per tick.
- SpinLock executed 10,000,000 iterations in 17,713,122 (ticks), which is 564 iterations per tick.
Result Set 2: Read/write access with an equal read and write frequency:
- Monitor executed 10,000,000 iterations in 23,701,864 (ticks), which is 421 executions iterations per tick.
- Interlocked executed 10,000,000 iterations in 19,864,150 (ticks), which is 503 iterations per tick.
- ReaderWriterLockSlim executed 10,000,000 iterations in 33,751,562 (ticks), which is 296 iterations per tick.
- MethodImpl executed 10,000,000 iterations in 43,442,379 (ticks), which is 230 iterations per tick.
- SpinLock executed 10,000,000 iterations in 20,713,905 (ticks), which is 482 iterations per tick.
Result Sets 3: Read-only access:
- Monitor executed 10,000,000 iterations in 13,810,171 (ticks), which is 724 iterations per tick.
- Interlocked executed 10,000,000 iterations in 17,628,526 (ticks), which is 567 iterations per tick.
- ReaderWriterLockSlim executed 10,000,000 iterations in 20,326,949 (ticks), which is 491 iterations per tick.
- MethodImpl executed 10,000,000 iterations in 34,891,919 (ticks), which is 286 iterations per tick.
- SpinLock executed 10,000,000 iterations in 7,046,085 (ticks), which is 1,419 iterations per tick.
The results are not surprising. Use of the MethodImplOptions.Synchronized option on static methods resulted in a lower concurrency rate. If you require thread synchronization for static methods then I strongly recommend you consider an alternate synchronization technique.
The full source code can be found here.