在C#多线程开发中,当多个线程需要同时操作集合时,保证集合的线程安全是核心需求。常见的实现方式有两种,一种是使用内置的线程安全集合ConcurrentBag,另一种是给普通的List加上lock锁来控制并发访问,这两种方式看似都能解决线程安全问题,实际存在诸多区别。

基本概念介绍
ConcurrentBag
ConcurrentBag是.NET Framework 4.0之后引入的线程安全集合,属于System.Collections.Concurrent命名空间,它是一个无序的对象集合,专门针对同一线程既生产又消费元素的场景做了优化,内部实现了无锁或细粒度锁的机制来保障线程安全。
List加lock
List是普通的泛型集合,本身不支持线程安全,当多个线程同时读写时会出现数据不一致的问题。给List加lock的思路是通过lock关键字对共享的List对象加锁,保证同一时间只有一个线程能访问集合,从而避免并发冲突。
核心区别对比
实现原理差异
ConcurrentBag内部采用了线程本地存储的机制,每个线程会维护自己的本地队列,当线程操作自己本地队列的元素时不需要加锁,只有在需要访问其他线程的队列或者本地队列元素不足时才会使用轻量级的锁进行同步,整体锁的粒度非常细。
而List加lock的方式,本质是用一个全局的锁对象(通常是List实例本身或者单独的object对象)来控制所有线程的访问,不管线程是读还是写操作,只要访问集合就需要获取锁,锁的粒度是全局的,并发度相对较低。
功能特性差异
ConcurrentBag提供了一些专门针对并发场景的方法,比如Add方法添加元素、TryTake方法尝试取出元素,这些方法内部已经处理了线程安全的逻辑,不需要开发者额外加锁。同时它支持枚举操作,但枚举过程中如果集合被修改,不会抛出异常,而是返回枚举开始时的集合快照。
给List加lock的方式,所有操作都需要手动包裹在lock代码块中,包括添加、删除、遍历等操作,如果遗漏了lock就可能出现线程安全问题。而且如果在lock块内部进行遍历,同时其他线程修改了List,会直接抛出InvalidOperationException异常。
性能表现差异
在单线程或者线程竞争不激烈的场景下,List加lock的性能可能略好,因为ConcurrentBag的内部实现有一定的额外开销。但是在多线程竞争激烈的场景下,尤其是每个线程频繁操作自己添加的元素时,ConcurrentBag的性能会远超List加lock,因为它的本地队列操作几乎无锁,减少了线程阻塞的概率。
我们可以通过一个简单的性能测试来直观对比两者的差异,测试场景是10个线程同时向集合中添加10000个元素:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 测试ConcurrentBag
ConcurrentBag<int> concurrentBag = new ConcurrentBag<int>();
Stopwatch sw1 = Stopwatch.StartNew();
Parallel.For(0, 10, _ =>
{
for (int i = 0; i < 10000; i++)
{
concurrentBag.Add(i);
}
});
sw1.Stop();
Console.WriteLine($"ConcurrentBag添加10万个元素耗时:{sw1.ElapsedMilliseconds}毫秒");
// 测试List加lock
List<int> list = new List<int>();
object lockObj = new object();
Stopwatch sw2 = Stopwatch.StartNew();
Parallel.For(0, 10, _ =>
{
for (int i = 0; i < 10000; i++)
{
lock (lockObj)
{
list.Add(i);
}
}
});
sw2.Stop();
Console.WriteLine($"List加lock添加10万个元素耗时:{sw2.ElapsedMilliseconds}毫秒");
}
}
实际运行后可以看到,在高并发添加元素的场景下,ConcurrentBag的耗时通常只有List加lock的十分之一甚至更少,性能优势非常明显。
适用场景选择
如果你的场景是多个线程都需要往集合中添加元素,同时每个线程也主要消费自己添加的元素,或者不需要保证集合的严格顺序,那么优先选择ConcurrentBag,它的性能和线程安全特性都更适配这类场景。
如果你的场景是多线程操作集合但竞争不激烈,或者需要集合保持插入顺序,或者需要频繁对集合进行遍历、查找等操作,而ConcurrentBag的无序特性无法满足需求,那么可以选择List加lock的方式,此时只需要注意所有操作都加上锁即可。
注意事项
- 使用ConcurrentBag时不需要额外加锁,再次加锁反而会破坏其性能优势,增加不必要的开销。
- 使用List加lock时,锁对象不要使用值类型,也不要使用public的对象,避免外部代码意外修改锁状态导致同步失效。
- ConcurrentBag的
TryTake方法在集合为空时会返回false,不会阻塞线程,如果需要阻塞等待元素,需要自己实现等待逻辑。
需要注意的是,不管是ConcurrentBag还是List加lock,都不是万能的线程安全方案,实际开发中需要根据具体的业务场景、并发量、功能需求来选择最合适的实现方式。
ConcurrentBagListlock线程安全修改时间:2026-06-30 18:45:32