【问题标题】:Why is lock much slower than Monitor.TryEnter?为什么 lock 比 Monitor.TryEnter 慢得多?
【发布时间】:2011-01-25 21:14:12
【问题描述】:

结果

锁定:85.3 微秒

Monitor.TryEnter:11.0 微秒

锁不是扩展成同样的代码吗?

编辑:1000 次迭代的结果: 锁定:103.3 微秒 Monitor.TryEnter:20.2 微秒

代码如下。谢谢

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

【问题讨论】:

  • 我不反对你的发现,但建议你尝试超过 100 次迭代。
  • 您能否重复相同的测试,但使用 Date.Now 代替(并使用适当大量的迭代将数字推入秒数)。我担心的是:support.microsoft.com/default.aspx?scid=KB;EN-US;Q274323
  • 有关信息,请注意TimeSpan.FromSeconds(10) 无助于获得简单的基准。

标签: c#


【解决方案1】:

可能是因为锁是 Monitor.Enter,而不是 Monitor.TryEnter?

【讨论】:

  • 与 Monitor.Enter 的结果相同。
【解决方案2】:

我实际上并不知道答案,但我觉得有必要指出 lockMonitor.TryEnter 在功能上等效。来自the MSDN documentation on Monitor.TryEnter

如果成功,此方法获取一个 obj 参数的排他锁。 该方法立即返回, 锁是否可用。

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


不管怎样,我刚刚在我的机器上尝试了你的代码,得到了完全不同的结果:

100 次迭代:
lock:4.4 微秒
Monitor.TryEnter:16.1 微秒
Monitor.Enter:3.9 微秒

100000 次迭代:
lock:2872.5 微秒
Monitor.TryEnter:5226.6 微秒
Monitor.Enter:2432.9 微秒

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


确实,我在针对 .NET 3.5 和 .NET 4.0 的 VS 2010 中尝试了此操作,尽管结果不同,但在每种情况下,lock 实际上都优于 Monitor.TryEnter

运行时版本:2.0.50727.3603

运行 100 次,每次 100000 次迭代:
锁定:279736.4 微秒
Monitor.TryEnter:1366751.5 微秒
Monitor.TryEnter(无超时):475107.3 微秒
Monitor.Enter:332334.1 微秒

运行时版本:4.0.30128.1

运行 100 次,每次 100000 次迭代:
锁定:334273.7 微秒
Monitor.TryEnter:1671363.4 微秒
Monitor.TryEnter(无超时):531451.8 微秒
Monitor.Enter:316693.1 微秒

(请注意,我还测试了 Monitor.TryEnter 没有超时,因为我同意 Marc 的观点,即调用 TimeSpan.FromSeconds 几乎可以肯定会减慢 Monitor.TryEnter 的时间——这些测试支持这一点——尽管这很奇怪,因为在你的的情况下显然lock仍然明显慢。)

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

【讨论】:

  • 这是我最初的假设。但这是错误的。 TryEnter 甚至比 lock 或 Enter 还要慢
  • @Andrey:事实证明,这也是我的发现。奇怪的是,我们的结果与 OP 的结果如此不同。
【解决方案3】:

100 太少了,在测试框架中运行可能会出现偏差。它也可能(参见 cmets)与与对象的 first 锁定相关的任何额外成本相关;试试:

  • 先在循环外锁定一次
  • 做更多的迭代
  • 在控制台 exe 中,在命令行中,在发布模式下

另外,请注意,在 4.0 中 lock 不是 Monitor.Enter(object) - 所以预计 4.0 中会出现不同的结果。

但我明白了:

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

来自测试台:

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

【讨论】:

  • 我认为完整的“厚”监视器只设置在第一个 竞争 锁上?否则它不只是在同步块中使用一个标志吗?
  • Marc,有任何参考资料可以证明“在 4.0 中的锁不是 Monitor.Enter”。这对我来说似乎不太可能。
  • @Steven:另见 Michał Bendowski 关于这个问题的帖子。
  • 啊,是的,我记得 Eric Lippert 的文章。根据您的回答,我假设 C# 4.0 不再使用 Monitor.Enter,但这并不是您实际所说的。你说得对,Monitor.Enter 的特定重载不再使用了。
【解决方案4】:

您可以使用 .NET 反射器来检查生成的 IL。 lock 关键字使用 Monitor.Enter 而不是 Monitor.TryEnter - 这是您问题的简短答案。以下是您的代码在反汇编并翻译回 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);
}

【讨论】:

  • 请注意,这大概是 C# 4.0 编译器 - 对于早期的编译器,IL 会略有不同(围绕调用的重载和 bool 本地)。
【解决方案5】:

如果您需要加快速度,那么根据我的经验,SpinLock 是一个更好的选择。

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 而不是“锁”对象,然后使用:

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

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

我还支持使用带有额外 if 语句的代码块编写的“try”案例:

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

SpinLock 对于高度竞争的锁不是 CPU 友好的,因为在这种情况下增加了对 SpinLock 的 CPU 使用,但是对于几乎同步并且只需要偶尔锁定外部引用或偶尔的第二个线程访问的锁,这是大获全胜。

是的,这并不华丽,但对我来说,SpinLocks 使我拥有的用于轻度竞争锁的所有功能都更加高效。

http://www.adammil.net/blog/v111_Creating_High-Performance_Locks_and_Lock-free_Code_for_NET_.html 很好地了解了自旋锁和整体锁定。

【讨论】:

  • 不要将 SpinLock 传递给方法/构造函数,因为它是一个值类型并且您传递了一个副本。这将导致完全不同步。要使此代码按预期工作,您需要使用 ref 修饰符。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-26
  • 2012-12-20
  • 1970-01-01
  • 2017-05-10
  • 1970-01-01
  • 1970-01-01
  • 2010-10-05
相关资源
最近更新 更多