线程安全的集合类 ConcurrentQueue、ConcurrentStack、BlockingCollection、ConcurrentBag、ConcurrentDictionary

在 .NET 中,ConcurrentQueue<T>ConcurrentStack<T>ConcurrentBag<T>BlockingCollection<T>ConcurrentDictionary<TKey, TValue> 都是线程安全的集合类,专为多线程并发场景设计,但它们的数据结构、功能特性和适用场景有显著差异。以下是详细对比:
image

核心特性对比表

类型 数据结构 核心功能 线程安全机制 阻塞行为 适用场景 性能特点
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(添加或更新)、TryRemoveTryUpdate 等。
    • 线程安全的遍历(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" // 键存在时的逻辑
);

选择建议

  1. 按数据结构选择

    • 需要顺序处理(FIFO)ConcurrentQueue<T>BlockingCollection<T>(默认队列)。
    • 需要逆序处理(LIFO)ConcurrentStack<T>BlockingCollection<T>(指定栈)。
    • 需要无序且高性能ConcurrentBag<T>(同一批线程生产消费)。
    • 需要键值对存储ConcurrentDictionary<TKey, TValue>
  2. 按阻塞需求选择

    • 不需要阻塞(空集合时返回 false) → 直接用 Concurrent* 集合。
    • 需要阻塞等待(空集合时线程暂停)或容量限制 → BlockingCollection<T>
  3. 按性能优先级选择

    • 最高性能(同一批线程操作) → ConcurrentBag<T>
    • 高并发键值对 → ConcurrentDictionary<TKey, TValue>
    • 顺序/逆序操作 → ConcurrentQueue<T>/ConcurrentStack<T>

这些类均无需手动加锁,内部通过优化的同步机制保证线程安全,是多线程编程的首选工具。

posted @ 2025-08-11 18:57  【唐】三三  阅读(45)  评论(0)    收藏  举报