线程安全的集合类 ConcurrentQueue、ConcurrentStack、BlockingCollection、ConcurrentBag、ConcurrentDictionary
在 .NET 中,ConcurrentQueue<T>
、ConcurrentStack<T>
、ConcurrentBag<T>
、BlockingCollection<T>
和 ConcurrentDictionary<TKey, TValue>
都是线程安全的集合类,专为多线程并发场景设计,但它们的数据结构、功能特性和适用场景有显著差异。以下是详细对比:
核心特性对比表
类型 | 数据结构 | 核心功能 | 线程安全机制 | 阻塞行为 | 适用场景 | 性能特点 |
---|---|---|---|---|---|---|
ConcurrentQueue<T> |
队列(FIFO) | 先进先出的线程安全存储 | 细粒度锁/无锁算法 | 无阻塞(TryDequeue 立即返回) |
多线程生产-消费(顺序处理) | 高(入队/出队操作轻量) |
ConcurrentStack<T> |
栈(LIFO) | 后进先出的线程安全存储 | 细粒度锁/无锁算法 | 无阻塞(TryPop 立即返回) |
多线程生产-消费(逆序处理,如撤销操作) | 高(栈操作本身简单) |
ConcurrentBag<T> |
无序集合 | 线程本地队列+全局集合的混合存储 | 线程本地存储+全局锁 | 无阻塞(TryTake 立即返回) |
同一批线程既生产又消费(如线程池任务共享) | 极高(优先操作本地队列,减少锁竞争) |
BlockingCollection<T> |
包装器(默认队列) | 对线程安全集合的包装,支持阻塞和容量控制 | 基于底层集合的同步机制 | 支持阻塞(Take 等待元素,Add 等待空间) |
需要阻塞等待或容量限制的生产-消费 | 中(额外的阻塞和边界控制逻辑) |
ConcurrentDictionary<TKey,TValue> |
字典(键值对) | 高并发的键值对存储 | 分段锁(按桶独立加锁) | 无阻塞(原子操作) | 多线程共享的缓存、计数器、配置表 | 极高(多线程可同时操作不同键,无锁竞争) |
详细说明
1. ConcurrentQueue<T>
- 数据结构:FIFO(先进先出)队列。
- 核心操作:
Enqueue
(入队)、TryDequeue
(出队,队列空时返回false
)、TryPeek
(查看队首元素)。 - 特点:严格保证元素顺序,适合需要按生产顺序处理的场景。
- 示例场景:日志收集(多线程写入日志,后台线程按顺序持久化)、任务调度(按提交顺序执行任务)。
var queue = new ConcurrentQueue<int>();
// 多线程入队
queue.Enqueue(1);
queue.Enqueue(2);
// 多线程出队
if (queue.TryDequeue(out int result))
{
Console.WriteLine($"出队元素: {result}"); // 必然是1(FIFO)
}
2. ConcurrentStack<T>
- 数据结构:LIFO(后进先出)栈。
- 核心操作:
Push
(入栈)、TryPop
(出栈,栈空时返回false
)、TryPeek
(查看栈顶元素)。 - 特点:元素按“后入先出”顺序处理,适合需要逆序操作的场景。
- 示例场景:撤销操作日志(最新操作先撤销)、递归任务的中间结果存储。
var stack = new ConcurrentStack<int>();
// 多线程入栈
stack.Push(1);
stack.Push(2);
// 多线程出栈
if (stack.TryPop(out int result))
{
Console.WriteLine($"出栈元素: {result}"); // 必然是2(LIFO)
}
3. ConcurrentBag<T>
- 数据结构:无序集合(内部为每个线程维护本地队列,减少锁竞争)。
- 核心操作:
Add
(添加元素)、TryTake
(获取并移除任意元素,集合空时返回false
)、TryPeek
(查看任意元素)。 - 特点:
- 无序性:无法保证元素的获取顺序。
- 高性能:
Add
优先加入当前线程的本地队列,TryTake
优先从本地队列获取,几乎无锁竞争。
- 示例场景:线程池任务共享临时数据(如多线程计算的中间结果)、同一批线程既生产又消费的场景。
var bag = new ConcurrentBag<int>();
// 多线程添加
bag.Add(1);
bag.Add(2);
// 多线程获取(顺序不确定)
if (bag.TryTake(out int result))
{
Console.WriteLine($"获取元素: {result}"); // 可能是1或2
}
4. BlockingCollection<T>
- 数据结构:包装器(默认包装
ConcurrentQueue<T>
,也可指定ConcurrentStack<T>
、ConcurrentBag<T>
等)。 - 核心操作:
Add
:添加元素(若设置容量边界,满时阻塞)。Take
:获取元素(空时阻塞,直到有元素可用)。CompleteAdding
:标记“不再添加元素”,消费者可通过IsCompleted
判断是否结束。
- 特点:
- 支持阻塞等待:解决
Concurrent*
集合“空队列时需轮询”的问题。 - 支持容量限制:防止集合无限增长导致内存溢出。
- 支持阻塞等待:解决
- 示例场景:
- 生产者-消费者管道(如下载→解析→存储,每个阶段用
BlockingCollection
衔接)。 - 需要严格控制并发量的场景(如限制队列最大长度为1000)。
- 生产者-消费者管道(如下载→解析→存储,每个阶段用
// 容量限制为2的阻塞集合(底层是队列)
var blocking = new BlockingCollection<int>(boundedCapacity: 2);
// 生产者(队列满时阻塞)
Task.Run(() => {
blocking.Add(1);
blocking.Add(2);
blocking.Add(3); // 此时队列满,阻塞等待
});
// 消费者(队列空时阻塞)
Task.Run(() => {
while (!blocking.IsCompleted)
{
int item = blocking.Take(); // 阻塞等待元素
Console.WriteLine($"处理元素: {item}");
}
});
5. ConcurrentDictionary<TKey, TValue>
- 数据结构:键值对字典(哈希表实现)。
- 核心操作:
- 原子操作:
GetOrAdd
(获取或添加)、AddOrUpdate
(添加或更新)、TryRemove
、TryUpdate
等。 - 线程安全的遍历(
GetEnumerator
返回快照)。
- 原子操作:
- 特点:
- 分段锁设计:将字典分为多个桶,每个桶独立加锁,多线程可同时操作不同键,性能接近单线程操作。
- 无锁竞争:不同键的操作互不干扰,适合高并发读写场景。
- 示例场景:
- 缓存系统(多线程读写缓存数据)。
- 计数器(如记录每个用户的访问次数)。
- 配置表(多线程读取,偶尔更新)。
var dict = new ConcurrentDictionary<int, string>();
// 原子操作:不存在则添加,存在则返回现有值
var value = dict.GetOrAdd(1, key => $"Value_{key}");
// 原子操作:更新或添加
dict.AddOrUpdate(1,
key => $"New_Value_{key}", // 键不存在时的逻辑
(key, oldValue) => $"{oldValue}_Updated" // 键存在时的逻辑
);
选择建议
-
按数据结构选择:
- 需要顺序处理(FIFO) →
ConcurrentQueue<T>
或BlockingCollection<T>
(默认队列)。 - 需要逆序处理(LIFO) →
ConcurrentStack<T>
或BlockingCollection<T>
(指定栈)。 - 需要无序且高性能 →
ConcurrentBag<T>
(同一批线程生产消费)。 - 需要键值对存储 →
ConcurrentDictionary<TKey, TValue>
。
- 需要顺序处理(FIFO) →
-
按阻塞需求选择:
- 不需要阻塞(空集合时返回
false
) → 直接用Concurrent*
集合。 - 需要阻塞等待(空集合时线程暂停)或容量限制 →
BlockingCollection<T>
。
- 不需要阻塞(空集合时返回
-
按性能优先级选择:
- 最高性能(同一批线程操作) →
ConcurrentBag<T>
。 - 高并发键值对 →
ConcurrentDictionary<TKey, TValue>
。 - 顺序/逆序操作 →
ConcurrentQueue<T>
/ConcurrentStack<T>
。
- 最高性能(同一批线程操作) →
这些类均无需手动加锁,内部通过优化的同步机制保证线程安全,是多线程编程的首选工具。