关于c#:为什么锁比Monitor.TryEnter慢得多?


Why is lock much slower than Monitor.TryEnter?

结果

锁定:85.3微秒

监视器.tryenter:11.0微秒

锁是否扩展为同一代码?

编辑:1000次迭代的结果:锁定:103.3微秒监视器.tryenter:20.2微秒

下面的代码。谢谢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    [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);;
    }


我实际上不知道答案,但我认为有必要指出,lockMonitor.TryEnter在功能上并不等同。从Monitor.TryEnter上的msdn文档中:

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

lock语句类似于Monitor.Enter,它可能会阻塞。当然,在您的示例代码中,不应该存在任何阻塞问题;但是我敢打赌,由于lock提供了阻塞,它所做的工作(可能)比TryEnter做的要多一些。

就其价值而言,我只是在我的机器上尝试了您的代码,得到了完全不同的结果:

100次迭代:lock:4.4微秒Monitor.TryEnter16.1微秒Monitor.Enter:3.9微秒

100000次迭代:lock:2872.5微秒Monitor.TryEnter5226.6微秒Monitor.Enter2432.9微秒

这严重破坏了我最初的猜测,表明在我的系统中,lock(其性能与Monitor.Enter)实际上大大优于Monitor.TryEnter

事实上,我在与2010年的对比中尝试将.NET 3.5和.NET 4.0作为目标,尽管结果不同,但在每种情况下,lock实际上都优于Monitor.TryEnter

运行时版本:2.0.50727.3603

运行100次,每次100000次迭代:锁定:279736.4微秒监视器.tryenter:1366751.5微秒monitor.tryenter(无超时):475107.3微秒监视器。输入:332334.1微秒

运行时版本:4.0.30128.1

运行100次,每次100000次迭代:锁定:334273.7微秒监视器.tryenter:1671363.4微秒monitor.tryenter(无超时):531451.8微秒监视器。输入:316693.1微秒

(注意,我也在没有超时的情况下测试了Monitor.TryEnter,正如我与Marc所同意的那样,调用TimeSpan.FromSeconds几乎肯定会减慢您使用Monitor.TryEnter的时间,这些测试支持这一点——尽管这很奇怪,因为在您的情况下,显然lock仍然明显较慢。)

基于这些结果,我强烈地倾向于相信,通过使用Test属性运行此代码,您所测量的执行时间会受到某种程度的影响。无论是那个代码还是这个代码,都要比我想象的更加依赖于机器。


100太少了,在测试框架中运行可能会使事情倾斜。它还可能(参见注释)与与对象的第一个锁相关联的任何附加成本相关;请尝试:

  • 先锁闭一次回路外
  • 做更多的迭代
  • 在控制台exe中,在命令行中,在释放模式下

另外,请注意,在4.0版中,lock不是Monitor.Enter(object),所以在4.0版中预期会有不同的结果。

但我得到:

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

从试验台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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");
    }
}


可以使用.NET Reflector检查生成的IL。lock关键字使用Monitor.Enter而不是Monitor.TryEnter,这里是您问题的简短答案。以下是当您的代码被反汇编并翻译回C时的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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);
}


可能是因为锁是monitor.enter,而不是monitor.tryenter?


如果你需要速度,那么旋转锁在我的经验中是一个更好的选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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();
            }
        }
    }
}

在中止和异常情况下,是让事情"自动"工作的一个很好的有用方法。

您只需创建一个spinlock而不是"lock"对象,然后使用:

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

这允许您获得lock()提供的同一行代码,同时还可以处理所需的try finally行为,并对清理对象所发生的事情有更多的控制。

我还支持"Try"案例,该案例将使用代码块编写,其中包含一个额外的if语句:

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

spinlock对于高度争用的锁不是CPU友好型的,因为在这种情况下,spinlock增加了CPU的使用,但是对于那些几乎是同步的,只是偶尔需要为外部引用或偶尔的第二线程访问锁定的锁,这是一个巨大的胜利。

是的,这不是华丽的,但对我来说,旋转锁使我拥有的所有东西都变得更具性能。

http://www.adammil.net/blog/v111_创建_-high-performance_-locks_和_-lock-free_-code_用于_-net_u.html可以很好地了解旋转锁和锁定的总体情况。