我一直在尝试多线程和并行处理,并且我需要一个计数器来进行一些基本的统计分析,以了解处理速度。为了避免使用我的类时出现并发问题,我在类的私有变量上使用了锁语句:
private object mutex = new object();
public void Count(int amount)
{
lock(mutex)
{
done += amount;
}
}
但是我想知道......锁定一个变量有多昂贵呢?对性能有什么负面影响吗?
我一直在尝试多线程和并行处理,并且我需要一个计数器来进行一些基本的统计分析,以了解处理速度。为了避免使用我的类时出现并发问题,我在类的私有变量上使用了锁语句:
private object mutex = new object();
public void Count(int amount)
{
lock(mutex)
{
done += amount;
}
}
但是我想知道......锁定一个变量有多昂贵呢?对性能有什么负面影响吗?
这里有一篇关于成本的文章。简短回答是50纳秒。
我想介绍几篇我写的文章,它们与通用同步原语有关,并深入探讨了监视器、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纳秒,其中锁定将变得无用。
考虑的另一个问题是它被测量为平均时间!
如果测量迭代的时间,那么将有1毫秒到20毫秒之间的时间,因为大多数是快速的,但是少数线程将等待处理器时间,并产生甚至长达几毫秒的延迟。
这对于任何需要高吞吐量、低延迟的应用程序来说都是一个坏消息。
最后需要考虑的问题是,在锁内部可能会出现较慢的操作,这种情况往往很常见。如果在锁内执行的代码块越长,争用就越高,延迟也会急剧上升。
请注意,从2003年到现在已经过去了十多年,这几代专门设计用于完全并发运行的处理器,而锁定显著地损害了它们的性能。
这并没有回答你关于性能的问题,但我可以说.NET Framework确实提供了一个Interlocked.Add
方法,它将允许你在不手动锁定其他对象的情况下将amount
添加到你的done
成员中。
lock
(Monitor.Enter / Exit)非常廉价,比等待句柄或互斥体等替代方案更为廉价。
但是,如果它速度变慢了(一点),您是否愿意拥有一个速度快但结果不正确的程序?
相比于没有锁的替代方案,一个在紧密循环中使用锁的成本是巨大的。你可以循环多次,仍然比使用锁更高效。这就是为什么无锁队列如此高效的原因。
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