lock语句的价格是多少?


111

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

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

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


10
锁定变量并不昂贵。您要避免等待锁定的变量。
加布

53
它比花费数小时追踪另一个比赛条件要便宜得多;-)
BrokenGlass 2011年

2
好吧...如果锁很昂贵,您可能希望通过更改编程来避免使用,以减少锁的数量。我可以实现某种同步。
Kees C. Bakker

1
仅仅通过将很多代码移出锁定块,我的性能有了显着提高(现在,在阅读@Gabe的评论之后)。底线:从现在开始,我将仅在变量块中保留变量访问权限(通常为一行),有点像“及时锁定”。是否有意义?
heltonbiker

2
@heltonbiker当然有道理。这也应该是体系结构原理,应该使锁尽可能短,简单和快速。仅需要同步的真正必要的数据。在服务器盒上,还应考虑锁的混合性质。即使对您的代码不是至关重要的争用也要归功于锁的混合特性,如果锁是由其他人持有的,则会导致内核在每次访问期间旋转。在线程被挂起之前,您正在有效地从服务器上的其他服务中吞噬一些CPU资源一段时间。
ipavlu

Answers:


86

这是一篇涉及成本的文章。简短的回答是50ns。


39
简短的更好答案:50ns +等待时间,如果其他线程保持锁定状态。
Herman

4
进入和离开锁的线程越多,获得的开销就越大。成本随着线程数量成倍增长
Arsen Zahray

16
一些上下文:在3Ghz x86上将两个数字相除大约需要10ns (不包括获取/解码指令所花费的时间);将单个变量从(非缓存的)内存加载到寄存器中大约需要40ns。因此50ns的速度惊人,快得令人难以置信 - lock与使用变量的成本相比,您不必担心使用任何其他成本。
BlueRaja-Danny Pflughoeft

3
同样,当问这个问题时,那篇文章太老了。
奥的斯2015年

3
真正伟大的指标,“几乎没有成本”,更何况是不正确的。你们没有考虑到它是短而快速的,并且只有在没有争用的情况下才是一个线程。在这种情况下,您根本不需要锁定。第二个问题,锁不是锁,而是混合锁,它在CLR内部检测到锁不是基于原子操作的任何人持有的,在这种情况下,它避免了对操作系统核心的调用,即不同的环无法对此进行测量测试。如果不采取锁定措施,则实际上是25ns至50ns的测量值是应用程序级别的互锁指令代码
ipavlu

50

从技术上来说,这是无法量化的,它在很大程度上取决于CPU内存回写缓冲区的状态以及必须丢弃并重新读取预取器收集的多少数据。两者都是不确定的。我使用150个CPU周期作为信封的近似值,以避免出现严重的失望。

实际的答案是,它是waaaay比的时候,你会燃烧在调试代码时,你认为你可以跳过锁量更便宜。

要获得一个硬数字,您必须进行测量。Visual Studio具有一个精巧的并发分析器作为扩展。


1
实际上,它可以量化和度量。只是不像在代码周围编写这些锁那样简单,然后说它们都只有50ns,这是对单线程访问锁的神话。
ipavlu 2015年

8
“认为您可以跳过锁” ……我认为很多人都在读这个问题的地方……
Snoop

30

进一步阅读:

我想介绍一些我对通用同步原语感兴趣的文章,并且它们将根据不同的场景和线程数来研究Monitor,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

原始答案:

噢亲爱的!

似乎这里标记为答案的正确答案本质上是不正确的!我想请答案的作者仔细阅读链接的文章。文章

2003年文章的作者仅在双核计算机上进行测量,在第一个测量案例中,他仅使用单线程测量了锁定,因此每次锁定访问的结果约为50ns。

它并没有说明并发环境中的锁定。因此,我们必须继续阅读该文章,下半年,作者正在测量具有两个和三个线程的锁定方案,该方案已接近当今处理器的并发级别。

因此,作者说,双核上有两个线程,锁的成本为120ns,而有3个线程,锁的成本为180ns。因此,这似乎显然取决于并发访问锁的线程数。

因此很简单,除非锁是无用的单线程,否则它不是50 ns。

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

如果要测量迭代时间,则仅在大多数情况下,这就是1ms到20ms之间的时间间隔,只是因为大多数线程速度很快,但是很少有线程会等待处理器时间,甚至会导致毫秒级的延迟。

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

最后要考虑的问题是,锁内部的操作可能会变慢,而且通常是这种情况。在锁内执行代码块的时间越长,争用就越多,延迟就越长。

请考虑,自2003年以来已经过去了十多年,专门设计用于完全并发运行的处理器只有几代了,而锁定会严重损害它们的性能。


1
需要澄清的是,本文并不是说锁性能会随着应用程序中线程数的增加而降低;争夺锁的线程数量会降低性能。(在上面的答案中暗含但没有明确说明。)
Gooseberry

我想您是说这句话的:“因此,它似乎显然取决于并发访问的线程数,而更糟糕的是。” 是的,措词可能更好。我的意思是“并发访问”是线程同时访问锁,从而引起争用。
ipavlu

20

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


1
是的,这可能是最好的答案。但是主要是因为代码更短,更简洁。速度差异不太明显。
Henk Holterman

感谢您的回答。我正在用锁做更多的事情。增加的整数是众多整数之一。爱这个建议,从现在开始将使用它。
Kees C. Bakker

即使无锁代码可能更快,锁也要容易得多。Interlocked.Add本身具有与+ =相同的问题,没有同步。
机库

10

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

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


5
哈哈...我正在追求快速的程序和良好的结果。
Kees C. Bakker

@ henk-holterman您的陈述有多个问题:首先,正如该问题和答案所清楚显示的那样,人们对锁对整体性能的影响了解甚少,甚至有人说约50ns的神话仅适用于单线程环境。其次,您的陈述在这里,并且将在内核中增长的处理器中保留多年,同时,内核的速度并不会那么快。******应用随着时间的推移只会变得更加复杂,然后它会逐层分层。锁定在许多内核的环境中,并且数量不断增加,2,4,8,10,20,16,32
ipavlu 2015年

我通常的方法是以松散耦合的方式建立同步,并尽可能减少交互。这对于无锁数据结构非常快。我为自旋锁设计了代码包装器,以简化开发,即使TPL具有特殊的并发集合,我也围绕列表,数组,字典和队列开发了自旋锁集合,因为我需要更多控制权,有时需要一些代码在自旋锁。我可以告诉您,这是有可能的,并允许解决TPL集合无法做到的许多情况,并具有出色的性能/吞吐量。
ipavlu 2015年

7

与不带锁的替代方案相比,紧密循环中的锁的成本很高。您可以承受多次循环,但仍然比锁更有效率。这就是为什么无锁队列如此高效的原因。

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

4
这可能是一个不好的例子,因为您的循环实际上不执行任何操作,除了单个变量分配和一个锁至少需要2个函数调用。而且,您获得的每个锁20ns也还不错。
Zar

5

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.