为什么ConcurrentBag <T>在.Net(4.0)中这么慢?我做错了吗?

Tac*_*chy 42 .net c# concurrency locking concurrent-collections

在我开始一个项目之前,我编写了一个简单的测试来比较来自(System.Collections.Concurrent)的ConcurrentBag相对于锁定和列表的性能.我非常惊讶ConcurrentBag比使用简单的List锁定慢10倍.据我所知,当读写器是同一个线程时,ConcurrentBag效果最好.但是,我没想到它的性能会比传统的锁更糟糕.

我已经运行了一个测试,其中有两个Parallel for循环写入和读取列表/包.然而,写入本身显示了巨大的差异:

private static void ConcurrentBagTest()
   {
        int collSize = 10000000;
        Stopwatch stopWatch = new Stopwatch();
        ConcurrentBag<int> bag1 = new ConcurrentBag<int>();

        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
        {
            bag1.Add(i);
        });


        stopWatch.Stop();
        Console.WriteLine("Elapsed Time = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
 }
Run Code Online (Sandbox Code Playgroud)

在我的盒子上,这需要3-4秒才能运行,相比之下这段代码的0.5-0.9秒:

       private static void LockCollTest()
       {
        int collSize = 10000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>(collSize);

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


        Parallel.For(0, collSize, delegate(int i)
            {
                lock(list1_lock)
                {
                    lst1.Add(i);
                }
            });

        stopWatch.Stop();
        Console.WriteLine("Elapsed = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
       }
Run Code Online (Sandbox Code Playgroud)

正如我所提到的,进行并发读写并不能帮助并发包测试.我做错了什么还是这个数据结构真的很慢?

[编辑] - 我删除了任务,因为我在这里不需要它们(完整代码有另一个任务阅读)

[编辑]非常感谢您的答案.我很难选择"正确的答案",因为它似乎是几个答案的混合.

正如Michael Goldshteyn指出的那样,速度实际上取决于数据.Darin指出应该有更多争用ConcurrentBag更快,而Parallel.For不一定会启动相同数量的线程.带走的一点是不要做任何事情,你不要一个锁内.在上面的例子中,我没有看到自己在锁内做任何事情,除非可能将值赋给temp变量.

另外,六个变量指出,碰巧运行的线程数也可能影响结果,尽管我尝试以相反的顺序运行原始测试,并且ConcurrentBag仍然较慢.

我在开始15个任务时运行了一些测试,结果取决于集合大小等.但是,ConcurrentBag的表现几乎与锁定列表一样好或更好,最多可达100万次插入.超过100万,锁定似乎有时更快,但我可能永远不会有一个更大的数据结构为我的项目.这是我运行的代码:

        int collSize = 1000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>();
        ConcurrentBag<int> concBag = new ConcurrentBag<int>();
        int numTasks = 15;

        int i = 0;

        Stopwatch sWatch = new Stopwatch();
        sWatch.Start();
         //First, try locks
        Task.WaitAll(Enumerable.Range(1, numTasks)
           .Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    lock (list1_lock)
                    {
                        lst1.Add(x);
                    }
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("lock test. Elapsed = {0}", 
            sWatch.Elapsed.TotalSeconds);

        // now try concurrentBag
        sWatch.Restart();
        Task.WaitAll(Enumerable.Range(1, numTasks).
                Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    concBag.Add(x);
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("Conc Bag test. Elapsed = {0}",
               sWatch.Elapsed.TotalSeconds);
Run Code Online (Sandbox Code Playgroud)

Dan*_*Tao 43

让我问你一个问题:你有一个应用程序不断添加到一个集合而从不读取它是多么现实?这样的收藏品有什么用?(这不是一个纯粹的反问.我想象有被使用,其中,例如,你只能从收集的关机(用于日志记录)或当用户提出要求.我相信,这些情况是相当罕见的,虽然读.)

这就是您的代码模拟的内容.List<T>.Add除了偶尔的情况,列表必须调整其内部数组的大小,所以除了偶尔的情况外,调用将是闪电般快速的.但是很快就会发生所有其他增加的问题.因此,在这种情况下,您不太可能看到大量的争用,尤其是在个人PC上进行测试,例如,甚至是8个核心(正如您在某处的评论中所述).也许你可能会看到像一个24核的机器,有许多内核可以尝试添加到列表的详细争硬是在同一时间.

从您的收藏中读取的地方,特别是争用的可能性更大.in foreach循环(或LINQ查询,相当于foreach引擎盖下的循环),需要锁定整个操作,以便您在迭代时不修改集合.

如果您能够真实地重现这种情况,我相信您会看到ConcurrentBag<T>比当前测试显示的更好的比例.


更新:是我编写的一个程序,用于比较上述场景中的这些集合(多个编写器,许多读者).运行25个试验,收集大小为10000和8个读取器线程,我得到以下结果:

Took 529.0095 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 39.5237 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
Took 309.4475 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 81.1967 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
Took 228.7669 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 164.8376 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
[ ... ]
Average list time: 176.072456 ms.
Average bag time: 59.603656 ms.

很明显,这取决于你对这些系列的确切做法.

  • 嘿,刚刚在研究ConcurrentBag时发现了这个问题,有趣的是,我从未从集合中读取过(直到线程完成写入并重新加入父级之后).特殊情况涉及将数据划分为多个集合 - 因此,可能并不像您想象的那么罕见;-) (4认同)

Pal*_*eta 15

在.NET Framework 4中似乎有一个错误,微软在4.5中修复了它,似乎他们没想到ConcurrentBag会被大量使用.

有关详细信息,请参阅以下Ayende帖子

http://ayende.com/blog/156097/the-high-cost-of-concurrentbag-in-net-4-0


Mic*_*eyn 9

作为一般答案:

  • 如果对其数据(即锁定)的争用很少或没有,则使用锁定的并发集合可以非常快.这是因为这样的集合类通常使用非常便宜的锁定原语构建,尤其是在没有条件的情况下.
  • 无锁收集可能会更慢,因为用于避免锁定的技巧和由于其他瓶颈(例如错误共享),实现其无锁性质导致缓存未命中所需的复杂性等等...

总而言之,哪种方式更快的决定高度依赖于所采用的数据结构以及锁之间的争用量以及其他问题(例如,num读取器与共享/排他类型排列中的写入器).

您的特定示例具有非常高的争用程度,因此我必须说我对此行为感到惊讶.另一方面,在保持锁定时完成的工作量非常小,因此可能毕竟没有争用锁本身.ConcurrentBag的并发处理的实现也可能存在缺陷,这使得您的特定示例(频繁插入和无读取)成为一个糟糕的用例.


use*_*116 9

使用MS的争用可视化工具查看该程序表明,ConcurrentBag<T>与并行插入相关的成本要高得多,而不是简单地锁定List<T>.我注意到的一件事是,似乎有一个成本与旋转6个线程(在我的机器上使用)开始第一次ConcurrentBag<T>运行(冷运行)相关.然后使用5或6个线程与List<T>代码,这是更快(热运行).ConcurrentBag<T>在列表后添加另一个运行显示它比第一个(热运行)花费的时间更少.

从我在争论中看到的,在ConcurrentBag<T>实现分配内存上花费了大量时间.从List<T>代码中删除显式的大小分配会减慢速度,但不足以产生影响.

编辑:它似乎是ConcurrentBag<T>内部保持每个列表Thread.CurrentThread,锁定2-4次,具体取决于它是否在新线程上运行,并执行至少一个Interlocked.Exchange.正如MSDN中所指出的那样:"针对同一个线程将产生和消耗存储在数据包中的数据的情况进行了优化." 对于您的性能下降与原始列表相比,这是最可能的解释.


Roh*_*hit 5

这已在.NET 4.5中得到解决.根本问题是ConcurrentBag使用的ThreadLocal并不期望有很多实例.这已经修复,现在可以运行得相当快.

source - .NET 4.0中ConcurrentBag的高成本

  • 那篇文章太可怕了; 作者正在测试创建大量ConcurrentBag <int>并将它们放入对象列表中,而不是实际测试制作1个袋子并将大量物品放入袋子本身. (3认同)