23

相关简要信息:

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();

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

baseline = 00:00:01.2604656
baseline = 00:00:00.3229741

问题 :

在单线程环境中是什么ConcurrentDictionary<,> 速度慢得多?

我的第一直觉是,这lock(){}总是会更慢。但显然不是。

4

8 回答 8

30

好吧,ConcurrentDictionary它允许多个线程使用它。对我来说,这似乎完全合理,这需要更多的内部管理,而不是假设它可以逃脱而不用担心来自多个线程的访问。如果反过来,我会感到非常惊讶——如果更安全的版本总是更快,你为什么要使用不太安全的版本?

于 2013-03-06T16:04:38.867 回答
28

最可能的原因只是比相同操作ConcurrentDictionary具有更多开销。Dictionary如果您深入研究来源,这显然是正确的

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

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

于 2013-03-06T16:05:49.850 回答
8

.NET 5 的更新:我将保留之前的答案,因为它仍然与较旧的运行时相关,但 .NET 5 似乎已经进一步改进ConcurrentDictionary到读取 viaTryGetValue()实际上比 normal 更快的程度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 |        - |        - |        - |         - |

以前的答案,仍然与 < .NET 5 相关:

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

你可以在这里阅读我的无锁线程安全的写时复制字典实现:

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

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

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

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

于 2018-11-13T08:46:45.237 回答
3

ConcurrentDictionary 与 Dictionary

通常,在您从多个线程同时添加和更新键或值的任何场景中使用 System.Collections.Concurrent.ConcurrentDictionary。在涉及频繁更新和相对较少读取的场景中,ConcurrentDictionary 通常提供适度的好处。在涉及多次读取和多次更新的场景中,ConcurrentDictionary 通常在具有任意数量内核的计算机上明显更快。

在涉及频繁更新的场景中,您可以在 ConcurrentDictionary 中提高并发度,然后在具有更多内核的计算机上测量性能是否会提高。如果更改并发级别,请尽可能避免全局操作。

如果您只读取键或值,则字典会更快,因为如果字典没有被任何线程修改,则不需要同步。

链接:https ://msdn.microsoft.com/en-us/library/dd997373%28v=vs.110%29.aspx

于 2015-05-27T04:54:15.577 回答
2

ConcurrentDictionary<>创建时会创建一组内部锁定对象(这由concurrencyLevel等因素决定)——这组锁定对象用于控制对一系列细粒度锁中内部存储桶结构的访问。

在单线程场景中,不需要锁,因此获取和释放这些锁的额外开销可能是您看到的差异的根源。

于 2013-03-06T16:05:21.473 回答
2

如果所有操作都在单个线程中完成,则在一个线程中使用 ConcurrentDictionary 或同步访问是没有意义的。当然字典会打败 ConcurrentDictionary。

很大程度上取决于使用模式和线程数。这是一个测试,表明 ConcurrentDictionary 的性能优于字典和锁,并随着线程数的增加而增加。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace ConsoleApp
{

    class Program
    {

        static void Main(string[] args)
        {
            Run(1, 100000, 10);
            Run(10, 100000, 10);
            Run(100, 100000, 10);
            Run(1000, 100000, 10);
            Console.ReadKey();
        }

        static void Run(int threads, int count, int cycles)
        {
            Console.WriteLine("");
            Console.WriteLine($"Threads: {threads}, items: {count}, cycles:{cycles}");

            var semaphore = new SemaphoreSlim(0, threads);

            var concurrentDictionary = new ConcurrentDictionary<int, string>();

            for (int i = 0; i < threads; i++)
            {
                Thread t = new Thread(() => Run(concurrentDictionary, count, cycles,  semaphore));
                t.Start();
            }

            Thread.Sleep(1000);

            var w = Stopwatch.StartNew();

            semaphore.Release(threads);

            for (int i = 0; i < threads; i++)
                semaphore.Wait();

            Console.WriteLine($"ConcurrentDictionary: {w.Elapsed}");

            var dictionary = new Dictionary<int, string>();
            for (int i = 0; i < threads; i++)
            {
                Thread t = new Thread(() => Run(dictionary, count, cycles, semaphore));
                t.Start();
            }

            Thread.Sleep(1000);

            w.Restart();

            semaphore.Release(threads);


            for (int i = 0; i < threads; i++)
                semaphore.Wait();

            Console.WriteLine($"Dictionary: {w.Elapsed}");

        }

        static void Run(ConcurrentDictionary<int, string> dic, int elements, int cycles, SemaphoreSlim semaphore)
        {
            semaphore.Wait();
            try
            {
                for (int i = 0; i < cycles; i++)
                    for (int j = 0; j < elements; j++)
                    {
                        var x = dic.GetOrAdd(i, x => x.ToString());
                    }
            }
            finally
            {
                semaphore.Release();
            }
        }

        static void Run(Dictionary<int, string> dic, int elements, int cycles, SemaphoreSlim semaphore)
        {
            semaphore.Wait();
            try
            {
                for (int i = 0; i < cycles; i++)
                    for (int j = 0; j < elements; j++)
                        lock (dic)
                        {
                            if (!dic.TryGetValue(i, out string value))
                                dic[i] = i.ToString();
                        }
            }
            finally
            {
                semaphore.Release();
            }
        }
    }
}

线程:1,项目:100000,周期:10 ConcurrentDictionary:00:00:00.0000499 字典:00:00:00.0000137

线程:10,项目:100000,周期:10 ConcurrentDictionary:00:00:00.0497413 字典:00:00:00.2638265

线程:100,项目:100000,周期:10 ConcurrentDictionary:00:00:00.2408781 字典:00:00:02.2257736

线程:1000,项目:100000,周期:10 ConcurrentDictionary:00:00:01.8196668 字典:00:00:25.5717232

于 2020-04-02T09:24:18.410 回答
0

ConcurrentDictionary<,>在单线程环境中是 什么让速度变慢了?

在多线程环境中使其速度更快所需的机器开销。

我的第一直觉是,这lock(){}总是会更慢。但显然不是。

Alock在没有竞争的情况下非常便宜。你可以lock每秒一百万次,你的 CPU 甚至都不会注意到,只要你是从一个线程中做的。在多线程程序中扼杀性能的是锁的争用。当多个线程激烈竞争同一个lock时,几乎所有线程都必须等待持有锁的幸运者释放它。这就是ConcurrentDictionary具有粒度锁定实现的 . 你拥有的并发性越多(处理器/内核越多),它就越闪耀。

于 2019-09-16T18:09:45.897 回答
-7

你的测试是错误的:你必须先停止秒表!

        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;
        sw.Stop();
        Console.WriteLine("baseline = " + sw.Elapsed);



        sw.Start();
        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;
        sw.Stop();
        Console.WriteLine("baseline = " + sw.Elapsed);

        sw.Stop();

- 输出 :

于 2015-06-17T14:16:59.250 回答