Why is lock much slower than Monitor.TryEnter?

前端 未结 5 574
囚心锁ツ
囚心锁ツ 2021-02-07 03:06

Results

Lock: 85.3 microseconds

Monitor.TryEnter: 11.0 microseconds

Isn\'t the lock expanded into the same code?

Edit: Results with 1000 iteratio

相关标签:
5条回答
  • 2021-02-07 03:23

    If you need speed doing this, then SpinLock is a far better choice in my experience.

    public class DisposableSpinLock : IDisposable {
        private SpinLock mylock;
        private bool isLocked;
    
        public DisposableSpinLock( SpinLock thelock ) {
            this.mylock = thelock;
            mylock.Enter( ref isLocked );
        }
    
        public DisposableSpinLock(  SpinLock thelock, bool tryLock) {
            this.mylock = thelock;
            if( tryLock ) {
                mylock.TryEnter( ref isLocked );
            } else {
                mylock.Enter( ref isLocked );
            }
        }
    
        public bool IsLocked { get { return isLocked; } }
    
        public void Dispose() {
            Dispose( true );
            GC.SuppressFinalize( this );
        }
    
        protected virtual void Dispose( bool disposing ) {
            if( disposing ) {
                if( isLocked ) {
                    mylock.Exit();
                }
            }
        }
    }
    

    Is a nice useful way to get things to work "automatically" in abort and exception cases.

    You can just create a SpinLock instead of the "lock" object, and then use:

    using( new DisposableSpinLock( myLock ) ) {
         // Under lock and key...
    }
    

    This allows you to get the same single line of code that lock() provides while also dealing with the required try {} finally{} behavior and have a bit more control over what happens to cleanup the object.

    I also have support for the "try" case which would be written using code blocks with an extra if statement inside:

    using( theLock = new DisposableSpinLock( myLock, true ) ) {
        if( theLock.IsLocked ) {
            // Under Lock and Key
        }
    }
    

    SpinLock is not not CPU friendly for highly contended locks because of the added CPU use of SpinLock in that situation, but for the locks that are pretty much synchronized and just need occasionally locking for outside references or occasional second thread access, this is a big win.

    Yes, this is not gorgeous, but for me, SpinLocks have made everything that I have for lightly contended locks much more performant.

    http://www.adammil.net/blog/v111_Creating_High-Performance_Locks_and_Lock-free_Code_for_NET_.html is a good look at spin locks and locking overall.

    0 讨论(0)
  • 2021-02-07 03:25

    100 is far too few, and running in a test framework may skew things. It is also possibly (see comments) related to any additional cost associated with the first lock against an object; try:

    • locking once outside the loop first
    • doing lots more iterations
    • in a console exe, at the command line, in release mode

    Also, note that in 4.0 lock is not Monitor.Enter(object) - so expect different results in 4.0.

    But I get:

    lock: 3548ms
    Monitor.TryEnter: 7008ms
    Monitor.TryEnter (2): 2947ms
    Monitor.Enter: 2906ms
    

    From the test rig:

    using System;
    using System.Diagnostics;
    using System.Threading;
    static class Program {
        static void Main()
        {
            const int lockIterations = 50000000;
            object object1 = new object();
            lock (object1) { Console.WriteLine("First one has to pay an extra toll"); }
            Stopwatch csLock = Stopwatch.StartNew();
            for (int i = 0; i < lockIterations; ) {
                lock (object1) { i++; }
            }
            csLock.Stop();
            Console.WriteLine("lock: " + csLock.ElapsedMilliseconds + "ms");
    
            Stopwatch csMonitorTryEnter = Stopwatch.StartNew();
            for (int i = 0; i < lockIterations; ) {
                if (Monitor.TryEnter(object1, TimeSpan.FromSeconds(10))) {
                    try { i++; } finally { Monitor.Exit(object1); }
                }
            }
            csMonitorTryEnter.Stop();
            Console.WriteLine("Monitor.TryEnter: " + csMonitorTryEnter.ElapsedMilliseconds + "ms");
    
            csMonitorTryEnter = Stopwatch.StartNew();
            for (int i = 0; i < lockIterations; ) {
                if (Monitor.TryEnter(object1, 10000)) {
                    try { i++; } finally { Monitor.Exit(object1); }
                }
            }
            csMonitorTryEnter.Stop();
            Console.WriteLine("Monitor.TryEnter (2): " + csMonitorTryEnter.ElapsedMilliseconds + "ms");
    
            Stopwatch csMonitorEnter = Stopwatch.StartNew();
            for (int i = 0; i < lockIterations; ) {
                Monitor.Enter(object1);
                try { i++; } finally { Monitor.Exit(object1); }
            }
            csMonitorEnter.Stop();
            Console.WriteLine("Monitor.Enter: " + csMonitorEnter.ElapsedMilliseconds + "ms");
        }
    }
    
    0 讨论(0)
  • 2021-02-07 03:31

    You can use .NET reflector to inspect the generated IL. The lock keyword uses Monitor.Enter instead of Monitor.TryEnter - here's the short answer to your question. Here's how your code looks like when disassembled and translated back to C#:

    public void Lock_Performance_Test()
    {
        Stopwatch csLock = Stopwatch.StartNew();
        int i = 0;
        while (i < 100)
        {
            object CS$2$0000;
            bool <>s__LockTaken0 = false;
            try
            {
                Monitor.Enter(CS$2$0000 = this.object1, ref <>s__LockTaken0);
                i++;
            }
            finally
            {
                if (<>s__LockTaken0)
                {
                    Monitor.Exit(CS$2$0000);
                }
            }
        }
        csLock.Stop();
        Stopwatch csMonitor = Stopwatch.StartNew();
        i = 0;
        while (i < 100)
        {
            if (Monitor.TryEnter(this.object1, TimeSpan.FromSeconds(10.0)))
            {
                try
                {
                    i++;
                }
                finally
                {
                    Monitor.Exit(this.object1);
                }
            }
        }
        csMonitor.Stop();
        Console.WriteLine("Lock: {0:f1} microseconds", csLock.Elapsed.Ticks / 10M);
        Console.WriteLine("Monitor.TryEnter: {0:f1} microseconds", csMonitor.Elapsed.Ticks / 10M);
    }
    
    0 讨论(0)
  • 2021-02-07 03:32

    may it be because lock is Monitor.Enter, not Monitor.TryEnter?

    0 讨论(0)
  • 2021-02-07 03:44

    I don't actually know the answer, but feel it's important to point out that lock and Monitor.TryEnter are not functionally equivalent. From the MSDN documentation on Monitor.TryEnter:

    If successful, this method acquires an exclusive lock on the obj parameter. This method returns immediately, whether or not the lock is available.

    The lock statement is analogous to Monitor.Enter, which does potentially block. Granted, in your example code, there shouldn't be any blocking issues; but I would wager that since lock provides blocking, it does a little more work (potentially) than TryEnter does.


    For what it's worth, I just tried your code on my machine and got completely different results:

    100 iterations:
    lock: 4.4 microseconds
    Monitor.TryEnter: 16.1 microseconds
    Monitor.Enter: 3.9 microseconds

    100000 iterations:
    lock: 2872.5 microseconds
    Monitor.TryEnter: 5226.6 microseconds
    Monitor.Enter: 2432.9 microseconds

    This seriously undermines my initial guess and shows that, on my system, lock (which performs about the same as Monitor.Enter) actually drastically outperforms Monitor.TryEnter.


    Indeed, I attempted this in VS 2010 targeting both .NET 3.5 and .NET 4.0 and, though the results were different, in each case lock did in fact outperform Monitor.TryEnter:

    Runtime version: 2.0.50727.3603

    Ran 100 times, 100000 iterations each time:
    Lock: 279736.4 microseconds
    Monitor.TryEnter: 1366751.5 microseconds
    Monitor.TryEnter (no timeout): 475107.3 microseconds
    Monitor.Enter: 332334.1 microseconds

    Runtime version: 4.0.30128.1

    Ran 100 times, 100000 iterations each time:
    Lock: 334273.7 microseconds
    Monitor.TryEnter: 1671363.4 microseconds
    Monitor.TryEnter (no timeout): 531451.8 microseconds
    Monitor.Enter: 316693.1 microseconds

    (Notice I also tested Monitor.TryEnter with no timeout, as I agreed with Marc that calling TimeSpan.FromSeconds was almost certainly slowing down your times for Monitor.TryEnter--and these tests support that--though it's strange, since in your case apparently lock is still significantly slower.)

    Based on these results I am strongly inclined to believe that your measured execution times are somehow affected by running this code with the Test attribute. Either that or this code is far more machine-dependent than I would have expected.

    0 讨论(0)
提交回复
热议问题