ConcurrentDictionary<比;单线程性能误区

本文关键字:性能 误区 单线程 ConcurrentDictionary | 更新日期: 2023-09-27 18:02:40

相关信息:

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(){}总是比较慢。但显然并非如此。

ConcurrentDictionary<比;单线程性能误区

嗯,ConcurrentDictionary允许可能性它可以被多个线程使用。在我看来,这需要更多的内部管理,而不是假设可以不担心来自多个线程的访问,这似乎是完全合理的。如果结果是相反的,我会非常惊讶——如果更安全的版本总是更快,为什么你要使用不太安全的版本呢?

最可能的原因是ConcurrentDictionaryDictionary有更多的开销。如果您深入研究源代码

,就可以证明这是正确的。
  • 使用索引器锁
  • 使用易失性写
  • 它必须对。net中不保证是原子性的值进行原子性写入
  • 在核心add例程中有额外的分支(是否取锁,是否原子写)

所有这些成本都是与使用它的线程数量无关的。这些成本可能单独很小,但不是免费的,并且随着时间的推移会增加

我将保留前面的答案,因为它仍然与旧的运行时相关,但是。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 |        - |        - |        - |         - |

上一个答案,仍然与

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

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

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

它目前只能追加(具有替换值的能力),因为它打算用作永久缓存。如果您需要删除,那么我建议使用ConcurrentDictionary,因为将其添加到CopyOnWriteDictionary中将消除由于添加锁定而获得的所有性能收益。

CopyOnWriteDictionary对于快速写入和查找来说非常快,通常在没有锁定的情况下以几乎标准的Dictionary速度运行。如果你偶尔写,经常读,这是最快的选择。

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

ConcurrentDictionary与Dictionary

一般使用aSystem.Collections.Concurrent.ConcurrentDictionary在添加和更新键或值的任何场景并发地从多个线程。在涉及频繁更新和相对较少的读取,ConcurrentDictionary通常提供适度的好处。在以下场景中多次读取和多次更新,ConcurrentDictionary通常在具有任意数量的核心。

在涉及频繁更新的场景中,可以增加在ConcurrentDictionary和然后测量计算机的性能是否会提高有更多的核。如果更改并发级别,请避免使用全局

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

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

ConcurrentDictionary<>在创建时创建了一组内部锁定对象(这是由concurrencyLevel决定的,以及其他因素)——这组锁定对象用于控制对一系列细粒度锁中的内部桶结构的访问。

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

在一个线程中使用ConcurrentDictionary或在一个线程中同步访问是没有意义的。当然字典会打败并发字典。

很大程度上取决于使用模式和线程数。下面是一个测试,显示随着线程数的增加,ConcurrentDictionary的性能优于dictionary和lock。

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

Threads: 1, items: 100000, cycles:10ConcurrentDictionary最后:00:00:00.0000499字典:00:00:00.0000137

Threads: 10, items: 100000, cycles:10ConcurrentDictionary最后:00:00:00.0497413字典:00:00:00.2638265

线程数:100,条目数:100000,循环数:10ConcurrentDictionary最后:00:00:00.2408781字典:00:00:02.2257736

Threads: 1000, items: 100000, cycles:10ConcurrentDictionary最后:00:00:01.8196668

是什么让ConcurrentDictionary<,>在单线程环境中更慢?

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

我的第一直觉是lock(){}总是比较慢。但显然并非如此。

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

在。net 4中,ConcurrentDictionary使用了非常差的锁管理和争用解析,这使得它非常慢。使用自定义锁和/或甚至使用TestAndSet来保存整个字典的速度更快。

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

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

——输出: