开发者

Why is lock much slower than Monitor.TryEnter?

开发者 https://www.devze.com 2022-12-22 23:38 出处:网络
Results Lock: 85.3 microseconds Monitor.TryEnter: 11.0 microseconds Isn\'t the lock expanded into the same code?

Results

Lock: 85.3 microseconds

Monitor.TryEnter: 11.0 microseconds

Isn't the lock expanded into the same code?

Edit: Results with 1000 iterations: Lock: 103.3 microseconds Monitor.TryEnter: 20.2 microseconds

Code below. Thanks

    [Test]
    public void Lock_Performance_Test()
    {
        const int lockIterations = 100;

        Stopwatch csLock = Stopwatch.StartNew();
        for (int i = 0; i < lockIterations; )
        {
            lock (object1)
            {
                i++;
            }
        }
        csLock.Stop();

        Stopwatch csMonitor = Stopwatch.StartNew();
        for (int i = 0; i < lockIterations; )
        {
            if (Monitor.TryEnter(object1, TimeSpan.FromSeconds(10)))
            {
                try
                {
                    开发者_如何学运维i++;
                }
                finally
                {
                    Monitor.Exit(object1);
                }
            }
        }
        csMonitor.Stop();

        Console.WriteLine("Lock: {0:f1} microseconds", csLock.Elapsed.Ticks / 10M);
        Console.WriteLine("Monitor.TryEnter: {0:f1} microseconds", csMonitor.Elapsed.Ticks / 10M);;
    }


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.


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");
    }
}


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


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);
}


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

精彩评论

暂无评论...
验证码 换一张
取 消