ConcurrentBag<MyType> Vs List<MyType>

32

使用 ConcurrentBag(Of MyType) 相对于仅使用 List(Of MyType) 有什么优势呢? MSDN页面上关于CB的内容指出

ConcurrentBag(Of T) 是一个线程安全的 袋(bag)实现,针对在同一线程中将生产和 消耗数据存储在袋中的情况进行了优化

那么这有什么优势呢?我能理解并发命名空间中其他集合类型的优势,但这个让我感到困惑。

5个回答

46

ConcurrentBag内部使用了多个不同的列表(Lists),每个列表对应一个写线程。

所引用的那个语句的意思是,在从Bag中读取元素时,会优先选择和当前线程对应的列表。也就是说,在冒险去争取其他线程列表的元素之前,它将首先检查当前线程的列表是否有该元素。

这样可以在多个线程同时读写时最小化锁竞争。当读取线程没有列表或其列表为空时,它必须锁定分配给其他线程的列表。但是,如果您有多个线程都从自己的列表中读取和写入元素,则永远不会出现锁竞争。


+1,这是正确的解释。另请参阅:http://www.codethinked.com/post/2010/01/27/NET-40-and-System_Collections_Concurrent_ConcurrentBag.aspx - Hans Passant

21
这里最大的优势是ConcurrentBag<T>可以安全地从多个线程访问,而List<T>则不行。如果线程安全访问对您的情况很重要,那么像ConcurrentBag<T>这样的类型可能比List<T>+手动锁定更有优势。在我们真正回答这个问题之前,我们需要了解一些关于您场景的详细信息。
此外,List<T>是一个有序集合,而ConcurrentBag<T>则不是。

8

简言之,我会说本地锁速度更快,但差异微不足道(或者我在设置测试时出了问题)。

性能分析:

private static IEnumerable<string> UseConcurrentBag(int count)
    {
        Func<string> getString = () => "42";

        var list = new ConcurrentBag<string>();
        Parallel.For(0, count, o => list.Add(getString()));
        return list;
    }

    private static IEnumerable<string> UseLocalLock(int count)
    {
        Func<string> getString = () => "42";
        var resultCollection = new List<string>();
        object localLockObject = new object();
        Parallel.For(0, count, () => new List<string>(), (word, state, localList) =>
        {
            localList.Add(getString());
            return localList;
        },
            (finalResult) => { lock (localLockObject) resultCollection.AddRange(finalResult); }
            );

        return resultCollection;
    }

    private static void Test()
    {
        var s = string.Empty;
        var start1 = DateTime.Now;
        var list = UseConcurrentBag(5000000);
        if (list != null)
        {
            var end1 = DateTime.Now;
            s += " 1: " + end1.Subtract(start1);
        }

        var start2 = DateTime.Now;
        var list1 = UseLocalLock(5000000);
        if (list1 != null)
        {
            var end2 = DateTime.Now;
            s += " 2: " + end2.Subtract(start2);
        }

        if (!s.Contains("sdfsd"))
        {
        }
    }

使用ConcurrentBag在5M条记录中运行3次并对其进行误差处理

" 1: 00:00:00.4550455 2: 00:00:00.4090409"
" 1: 00:00:00.4190419 2: 00:00:00.4730473"
" 1: 00:00:00.4780478 2: 00:00:00.3870387"

使用Local lock在5M条记录上运行ConcurrentBag 3次:

" 1: 00:00:00.5070507 2: 00:00:00.3660366"
" 1: 00:00:00.4470447 2: 00:00:00.2470247"
" 1: 00:00:00.4420442 2: 00:00:00.2430243"

使用50M条记录:

" 1: 00:00:04.7354735 2: 00:00:04.7554755"
" 1: 00:00:04.2094209 2: 00:00:03.2413241"

我认为本地锁稍微更快一些。

更新: 在(Xeon X5650 @ 2.67GHz 64bit Win7 6 core)上,“本地锁”表现得更好。

使用50M条记录:

1: 00:00:09.7739773 2: 00:00:06.8076807
1: 00:00:08.8858885 2: 00:00:04.6184618
1: 00:00:12.5532552 2: 00:00:06.4866486


我很想看看这个程序如何扩展。随着核心数量的增加,锁竞争会增加时间。ConcurrentBag将为每个线程使用一个List,因此不会增加锁竞争(尽管需要更多内存,这是一个古老的权衡)。 - MattC
@MattC 你好,我在 E3-1225 v3 @ 3.20Ghz 16GB 64bit Win7 上运行了这个程序。这是一台拥有4个核心的CPU。如果你能找到不同核心数量的机器,请运行上述代码,我很乐意将结果合并到答案中。谢谢。 - Matas Vaitkevicius
5000万条记录。Xeon X5650 @ 2.67GHz 64位Win7 6核在我的机器上非常不稳定! - MattC
@MattC 比率足够接近,可以确认“本地锁定”更快(9.7739773/6.8076807 = 1.43572792713,8.8858885/4.6184618 = 1.92399307059,12.5532552/6.4866486 = 1.9352451) - Matas Vaitkevicius
如果你只是在添加列表,那么我同意使用锁。 - MattC

3

与其他并发集合不同,ConcurrentBag<T>被优化为单线程使用。
List<T>不同,ConcurrentBag<T>可以同时从多个线程中使用。


2
我认为你应该将它理解为“多个线程访问容器,每个线程可能同时生成和/或消费数据”,这确实是用于并行场景的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接