在开始一个项目之前,我编写了一个简单的测试来比较(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);
 }

在我的盒子上,这需要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);
       }

正如我提到的,进行并发读取和写入对并发袋测试没有帮助。我是在做错什么,还是这个数据结构真的很慢?

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

[编辑]
非常感谢您的回答。我很难选择“正确的答案”,因为这似乎是几个答案的组合。

正如Michael Goldshteyn所指出的,速度实际上取决于数据。
Darin指出,应该有更多的争用让ConcurrentBag更快,而Parallel.For不必启动相同数量的线程。要带走的一点是,不要在锁中不执行不需要的任何事情。在上述情况下,我看不到自己在锁内执行任何操作,除非可能是将值分配给temp变量。

另外,sixlettervariables指出,尽管我尝试以相反的顺序运行原始测试并且ConcurrentBag仍然较慢,但是碰巧正在运行的线程数也可能会影响结果。

我从启动15个任务开始进行了一些测试,结果取决于其他因素。但是,对于最多一百万次插入,ConcurrentBag的性能几乎与锁定列表一样好或比后者更好。超过一百万,锁定有时看起来会快得多,但是我的项目可能永远不会拥有更大的数据结构。
这是我运行的代码:
        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);

最佳答案

让我问你一个问题:拥有一个不断添加到集合而从不读取的应用程序有多现实?这样的集合有什么用? (这不是一个纯粹的修辞问题。我可以想象有一些用途,例如,您仅在关机(用于日志记录)时或在用户请求时才从集合中读取内容。不过,我相信这些情况很少见。

这就是您的代码正在模拟。除了偶尔需要调整列表内部数组大小的情况外,调用List<T>.Add将会非常快。但这很快就被所有其他添加所消除。因此,在这种情况下,您不太可能看到大量竞争,尤其是在具有甚至8个内核的个人PC上进行测试(正如您在某处的评论中所指出的那样)。也许您会在24核计算机之类的设备上看到更多争用,其中许多核可尝试同时从字面上添加到列表中。

尤其是在您从馆藏中读取内容时,争用很有可能蔓延。在foreach循环(或LINQ查询,相当于幕后的foreach循环)中,它们需要锁定整个操作,以便在迭代时无需修改集合。

如果您可以现实地重现这种情况,我相信您会看到ConcurrentBag<T>比例比当前测试显示的要好得多。

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

花费529.0095 ms,将10000个元素添加到具有8个阅读器线程的List 中。
花费39.5237毫秒,将10000个元素添加到具有8个读取器线程的ConcurrentBag 中。
花费309.4475毫秒将10000个元素添加到具有8个阅读器线程的List 中。
花费81.1967毫秒将10000个元素添加到具有8个读取器线程的ConcurrentBag 中。
花费228.7669 ms,将10000个元素添加到具有8个阅读器线程的List 中。
花费164.8376毫秒将10000个元素添加到具有8个读取器线程的ConcurrentBag 中。
[...]
平均列表时间:176.072456毫秒。
平均布袋时间:59.603656毫秒。

因此很明显,这完全取决于您对这些集合的处理方式。

10-07 23:32