锁语句有多贵?

Posted

技术标签:

【中文标题】锁语句有多贵?【英文标题】:How expensive is the lock statement? 【发布时间】:2011-06-08 02:24:20 【问题描述】:

我一直在试验多线程和并行处理,我需要一个计数器来对处理速度进行一些基本的计数和统计分析。为了避免并发使用我的类时出现问题,我在我的类中的私有变量上使用了 lock 语句:

private object mutex = new object();

public void Count(int amount)

 lock(mutex)
 
  done += amount;
 

但我想知道...锁定变量有多昂贵?对性能有什么负面影响?

【问题讨论】:

锁定变量并不昂贵;这是您要避免的锁定变量的等待。 这比花费数小时追踪另一个比赛条件要便宜得多;-) 好吧...如果锁很昂贵,您可能希望通过更改编程来避免它们,以便它需要更少的锁。我可以实现某种同步。 仅通过将大量代码移出我的锁块,我的性能(现在,在阅读 @Gabe 的评论后)有了显着的提高。底线:从现在开始,我将只将变量访问(通常是一行)留在锁定块内,有点像“及时锁定”。有意义吗? @heltonbiker 当然有道理。这也应该是架构原则,你应该使锁尽可能短、简单和快速。只有真正需要同步的数据。在服务器盒上,您还应该考虑锁的混合特性。即使对您的代码来说不是关键的争用也是由于锁的混合性质,如果锁被其他人持有,导致内核在每次访问期间旋转。在你的线程被挂起之前,你实际上是在消耗服务器上其他服务的一些 cpu 资源。 【参考方案1】:

这里是 an article 成本。简短的回答是 50ns。

【讨论】:

简短的更好的答案:50ns + 等待其他线程持有锁所花费的时间。 进入和离开锁的线程越多,它的开销就越大。成本随着线程数呈指数增长 一些上下文:在 3Ghz x86 上除以两个数字大约需要 10ns (不包括获取/解码指令所需的时间);并且将单个变量从(非缓存)内存加载到寄存器中大约需要 40ns。所以 50ns 非常快,令人眼花缭乱快 - 你不必担心使用 lock 的成本,就像担心使用变量的成本一样。 另外,当被问到这个问题时,那篇文章已经过时了。 非常棒的指标,“几乎没有成本”,更不用说不正确了。你们没有考虑到,它只是短而快,而且只有在根本没有争用的情况下,一个线程。在这种情况下,您根本不需要锁定。第二个问题,锁不是锁,而是混合锁,它在CLR内部检测到锁没有被任何人基于原子操作持有,在这种情况下,它避免了对操作系统核心的调用,即不同的环,这些环没有被测量测试。 25ns 到 50ns 测量的实际上是应用程序级联锁指令代码,如果不采取锁定【参考方案2】:

技术上的答案是,这是无法量化的,它在很大程度上取决于 CPU 内存回写缓冲区的状态以及预取器收集的数据有多少必须被丢弃和重新读取。这两者都是非常不确定的。我使用 150 个 CPU 周期作为粗略的近似值,以避免重大失望。

实际的答案是,waaaay 比您认为可以跳过锁时调试代码所花费的时间要便宜。

要获得一个硬性数字,您必须进行测量。 Visual Studio 有一个漂亮的 concurrency analyzer 可用作扩展。

【讨论】:

其实没有,是可以量化和衡量的。这并不像在代码周围写下这些锁那么容易,然后声明它只有 50ns,这是一个基于单线程访问锁的神话。 "think you can skip a lock"...我想很多人在阅读这个问题时都会遇到这种情况...【参考方案3】:

延伸阅读:

我想介绍几篇我的文章,它们对一般同步原语感兴趣,他们正在深入研究监视器、C# 锁定语句行为、属性和成本,具体取决于不同的场景和线程数。它对 CPU 浪费和吞吐量周期特别感兴趣,以了解在多种情况下可以完成多少工作:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https://www.codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

原答案:

天哪!

似乎此处标记为 THE ANSWER 的正确答案本质上是不正确的!我想请答案的作者将链接的文章阅读到最后。 article

2003 年的文章article 的作者仅在双核机器上进行了测量,在第一个测量案例中,他仅测量了单线程锁定,结果是每个锁定大约 50ns访问。

它没有说明并发环境中的锁。 所以我们必须继续阅读这篇文章,在后半部分,作者正在测量两个线程和三个线程的锁定场景,这更接近当今处理器的并发水平。

所以作者说,双核上有两个线程,锁花费 120ns,而 3 个线程则达到 180ns。所以它似乎显然取决于并发访问锁的线程数。

所以很简单,不是 50 ns,除非它是单线程,否则锁会变得毫无用处。

另一个需要考虑的问题是它是以平均时间来衡量的!

如果要测量迭代时间,甚至会有 1 毫秒到 20 毫秒之间的时间,这仅仅是因为大多数速度很快,但很少有线程会等待处理器时间,甚至会产生几毫秒的长延迟。

这对于任何需要高吞吐量、低延迟的应用程序来说都是个坏消息。

最后一个需要考虑的问题是锁内的操作可能会变慢,而且这种情况经常发生。 代码块在锁内执行的时间越长,争用就越高,延迟就会飙升。

请考虑,从 2003 年到现在已经过去了十多年,那是几代专门设计为完全并发运行的处理器,而锁定严重损害了它们的性能。

【讨论】:

澄清一下,这篇文章并不是说锁性能会随着应用程序中的线程数而降低;性能会随着争夺锁的线程数量而降低。 (在上面的答案中暗示但没有明确说明。) 我想你的意思是:“所以它似乎显然取决于并发访问的线程数,而且越多越糟糕。”是的,措辞可以更好。我的意思是“并发访问”,因为线程同时访问锁,从而产生争用。【参考方案4】:

这并不能回答您关于性能的查询,但我可以说 .NET Framework 确实提供了一个 Interlocked.Add 方法,该方法允许您将您的 amount 添加到您的 done 成员中,而无需手动锁定另一个成员对象。

【讨论】:

是的,这可能是最好的答案。但主要是因为代码更短更简洁。速度上的差异不太可能很明显。 感谢您的回答。我正在用锁做更多的事情。添加的整数是其中之一。喜欢这个建议,从现在开始会使用它。 锁更容易正确,即使无锁代码可能更快。 Interlocked.Add 本身与 += 有相同的问题,但没有同步。 无锁并不是“可能更快”。在极其紧凑、长时间运行的并发循环中,它可以快几个数量级。【参考方案5】:

lock (Monitor.Enter/Exit) 非常便宜,比 Waithandle 或 Mutex 等替代品便宜。

但是如果它(有点)慢怎么办,你宁愿有一个结果不正确的快速程序吗?

【讨论】:

哈哈...我本来打算快速的程序和好的结果。 @henk-holterman 您的陈述存在多个问题:首先正如这个问答清楚地表明的那样,人们对锁定对整体性能的影响了解甚少,甚至人们陈述了关于 50ns 的神话,这仅适用于单线程环境。 第二您的声明在这里并将持续数年,同时,处理器在内核中增长,但内核速度并没有那么多。**第三**应用程序只会随着时间的推移变得更加复杂,并且然后在多核环境下层层锁,数量不断上升,2,4,8,10,20,16,32 我通常的做法是以松耦合的方式构建同步,尽可能少的交互。这对于无锁数据结构来说非常快。我围绕自旋锁制作了我的代码包装器以简化开发,即使 TPL 有特殊的并发集合,我也围绕列表、数组、字典和队列开发了我自己的自旋锁定集合,因为我需要更多的控制,有时需要一些代码在下面运行自旋锁。我可以告诉你,它是可能的并且允许解决 TPL 集合无法解决的多个场景,并且具有出色的性能/吞吐量增益。【参考方案6】:

与没有锁的替代方案相比,在紧密循环中使用锁的成本是巨大的。您可以承受多次循环,并且仍然比锁更有效。这就是无锁队列如此高效的原因。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication

    class Program
    
        static void Main(string[] args)
        
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: 0", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: 0", stopwatch.ElapsedMilliseconds);
            

            Console.ReadKey();
        
    

输出:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208

【讨论】:

这可能是一个不好的例子,因为你的循环真的没有做任何事情,除了一个变量赋值和一个锁至少是 2 个函数调用。此外,每次锁定 20ns 也不错。【参考方案7】:

有几种不同的方式来定义“成本”。有获取和释放锁的实际开销;正如 Jake 所写,除非此操作执行数百万次,否则这可以忽略不计。

更相关的是这对执行流程的影响。此代码一次只能由一个线程输入。如果您有 5 个线程定期执行此操作,其中 4 个将最终等待锁被释放,然后在锁被释放后成为第一个调度进入该段代码的线程。因此,您的算法将受到严重影响。多少取决于算法以及调用操作的频率。如果不引入竞争条件,您无法真正避免它,但您可以通过最小化对锁定代码的调用次数来改善它。

【讨论】:

以上是关于锁语句有多贵?的主要内容,如果未能解决你的问题,请参考以下文章

java方法调用有多贵

IOS - reloadData 有多贵?

NULL 指针参数有多贵?

线程有多贵?

“nodetool repair”操作有多贵?

命名管道(fifo)有多贵?