ConcurrentDictionary <>在一个线程误解的表现?

Roy*_*mir 17 c# .net-4.0 task-parallel-library concurrentdictionary

相关简介信息:

AFAIK,并发堆栈,队列和包类在内部使用链接列表实现.
而且我知道争用少得多,因为每个线程都负责自己的链表.无论如何,我的问题是关于ConcurrentDictionary<,>

但我正在测试这段代码:(单线程)

Stopwatch sw = new Stopwatch();
sw.Start();

    var d = new ConcurrentDictionary < int,  int > ();
    for(int i = 0; i < 1000000; i++) d[i] = 123;
    for(int i = 1000000; i < 2000000; i++) d[i] = 123;
    for(int i = 2000000; i < 3000000; i++) d[i] = 123;
    Console.WriteLine("baseline = " + sw.Elapsed);

sw.Restart();

    var d2 = new Dictionary < int, int > ();
    for(int i = 0; i < 1000000; i++)         lock (d2) d2[i] = 123;
    for(int i = 1000000; i < 2000000; i++)   lock (d2) d2[i] = 123;
    for(int i = 2000000; i < 3000000; i++)   lock (d2) d2[i] = 123;
    Console.WriteLine("baseline = " + sw.Elapsed);

sw.Stop();
Run Code Online (Sandbox Code Playgroud)

结果:( 多次测试,相同值(+/-)).

baseline = 00:00:01.2604656
baseline = 00:00:00.3229741
Run Code Online (Sandbox Code Playgroud)

题 :

是什么ConcurrentDictionary<,> 很多较慢单一线程环境中?

我的第一直觉是lock(){}总是会慢一些.但显然事实并非如此.

Jon*_*eet 26

那么,ConcurrentDictionary被允许的可能性,它可以被多个线程使用.对我来说,这需要更多的内部管理而不是假设它可以逃脱而不用担心多线程的访问,这似乎是完全合理的.如果反过来已经解决了,我会感到非常惊讶 - 如果更安全的版本总是更快,为什么你会使用安全性较低的版本?

  • @JonSkeet我不太确定.谁会想要开一辆跑车的坦克!? (6认同)
  • @Servy:在坦克和跑车之间进行更好的比较.坦克更坚固,但通常会更慢...... (5认同)
  • 或许,差异的程度让他吃惊? (2认同)
  • @M.kazemAkhgary:我没有看到数字表明从多个线程锁定字典比使用“ConcurrentDictionary”更快。这当然不是问题所显示的。同样值得理解的是,除了此处显示的操作之外,还有更多操作 - 诸如遍历字典之类的操作,您不会希望在整个迭代时间内获取锁。 (2认同)

Jar*_*Par 24

最可能的原因是,ConcurrentDictionary开销比Dictionary同一操作更多.如果你深入研究资料来源,这显然是正确的

  • 它使用锁定索引器
  • 它使用易失性写入
  • 它必须对值进行原子写入,这些值不能保证在.Net中是原子的
  • 它在核心添加例程中有额外的分支(是否采取锁定,进行原子写入)

无论所使用的线程数是多少,都会产生所有这些成本.这些成本可能单独很小,但不是免费的,并且随着时间的推移会增加

  • @RoyiNamir我和伊戈尔聊天,他说确实特定的线路与ARM有关.他最近为MSDN杂志撰写了2篇文章,他说这篇文章更新,并且代表了ARM http://msdn.microsoft.com/en-us/magazine/jj863136.aspx http://msdn.microsoft.com/ EN-US /杂志/ jj883956.aspx (2认同)

Mik*_*ski 7

.NET 5 的更新:我将保留之前的答案,因为它仍然与较旧的运行时相关,但 .NET 5 似乎已经进一步改进ConcurrentDictionary到读取通过TryGetValue()实际上比正常更快的地步Dictionary,如结果所示下面(COW 是我的CopyOnWriteDictionary,详述如下)。做你想做的:)

|          Method |        Mean |     Error |    StdDev |    Gen 0 |    Gen 1 |    Gen 2 | Allocated |
|---------------- |------------:|----------:|----------:|---------:|---------:|---------:|----------:|
| ConcurrentWrite | 1,372.32 us | 12.752 us | 11.304 us | 226.5625 |  89.8438 |  44.9219 | 1398736 B |
|        COWWrite | 1,077.39 us | 21.435 us | 31.419 us |  56.6406 |  19.5313 |  11.7188 |  868629 B |
|       DictWrite |   347.19 us |  5.875 us |  5.208 us | 124.5117 | 124.5117 | 124.5117 |  673064 B |
|  ConcurrentRead |    63.53 us |  0.486 us |  0.431 us |        - |        - |        - |         - |
|         COWRead |    81.55 us |  0.908 us |  0.805 us |        - |        - |        - |         - |
|        DictRead |    70.71 us |  0.471 us |  0.393 us |        - |        - |        - |         - |
Run Code Online (Sandbox Code Playgroud)

上一个答案,仍然与 < .NET 5 相关:

ConcurrentDictionary自从我最初发布此答案以来,最新版本已显着改进。它不再锁定读取,因此提供与我的CopyOnWriteDictionary实现几乎相同的性能配置文件,但具有更多功能,因此我建议您在大多数情况下使用它。ConcurrentDictionary仍然比Dictionaryor多出 20 - 30% 的开销CopyOnWriteDictionary,因此对性能敏感的应用程序仍然可以从它的使用中受益。

您可以在此处阅读有关我的无锁线程安全写时复制字典实现的信息:

http://www.singulink.com/CodeIndex/post/fastest-thread-safe-lock-free-dictionary

它目前是仅附加的(具有替换值的能力),因为它旨在用作永久缓存。如果您需要删除,那么我建议使用,ConcurrentDictionary因为将其添加到CopyOnWriteDictionary会消除由于添加锁定而导致的所有性能提升。

CopyOnWriteDictionary对于快速突发的写入和查找非常快,通常以几乎Dictionary没有锁定的标准速度运行。如果您偶尔写作并经常阅读,这是可用的最快选项。

我的实现通过在正常情况下消除对任何读取锁的需求,同时不对字典进行更新来提供最大的读取性能。权衡是在应用更新后需要复制和交换字典(这是在后台线程上完成的)但是如果你不经常写或者你在初始化期间只写一次那么权衡绝对值得它。