马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
12. 并发聚集与线程安全范例
本章 GitHub 堆栈:csharp-concurrency-cookbook ⭐
欢迎 Star 和 Fork!全部代码示例都可以在堆栈中找到并运行。
🎯 本章导读
📌 本文目标:搞清晰 System.Collections.Concurrent 定名空间里那些并发聚集的精确用法和内部原理,相识 Channel 这个当代生产者-斲丧者利器,以及不可变聚集(ImmutableCollections)的线程安全之道。
上一篇中,我们把 C# 里的各种锁摸了个遍——从 lock 到 SemaphoreSlim,从 SpinLock 到 AsyncLock。但是,大概有人问这个标题:
"我只是想在多线程里维护一个字典 / 队列,岂非每次我都要本身加锁吗?"
答案固然是:不消。.NET 早就替你把这部门活儿干了,这就是本日要聊的——并发聚集。
不外,并发聚集不是银弹。它们各自有适当的场景,也有许多初学者踩过的坑。本日我们就体系地把这张牌打开,让你对每一张都心田有数。
🤔 先想想:为什么平凡 List/Dictionary 不是线程安全的?
这是个好标题,许多人用了好久并发聚集,却从没认真想过这一层。
以 List.Add 为例
我们来看看 List 内部大概长什么样(简化版):- // List<T> 内部核心字段(简化)
- private T[] _items; // 底层数组
- private int _size; // 当前元素个数
- public void Add(T item)
- {
- // 第一步:容量不够时扩容(翻倍策略,创建新数组并复制旧数据)
- if (_size == _items.Length)
- EnsureCapacity();
- // 第二步:写入数据
- _items[_size] = item;
- // 第三步:更新计数
- _size++;
- }
复制代码 现在想象两个线程同时调用 Add,_size 当前是 5:- 线程A:读取 _size = 5
- 线程B:读取 _size = 5
- 线程A:_items[5] = "A"
- 线程B:_items[5] = "B" ← 覆盖了线程A写的!
- 线程A:_size = 6
- 线程B:_size = 6 ← 两次Add,_size只增加了1,数据丢失!
复制代码 这就是经典的 竞态条件(Race Condition)。更糟糕的是,当 EnsureCapacity 触发扩容时,线程A在复制旧数组,线程B在往旧数组写数据,效果大概是 IndexOutOfRangeException,也大概是数据静默丢失——而你完全不知道发生了什么。
那 Dictionary 呢?
更贫苦。字典内部是哈希表,涉及到:
- 盘算哈希值
- 找到 bucket(桶)
- 处理处罚哈希辩说(链表/开放寻址)
- 扩容(rehash,整个数据结构重修)
任何一个步调被中途打断,都大概导致:
- 数据丢失
- 哈希链表成环(死循环!.NET Framework 4.0 从前确实有这个 bug)
- 抛出 InvalidOperationException(聚集在摆列过程中被修改)
结论:平凡聚集的筹划目标是单线程高性能,它们为了镌汰开销,完全没有思量线程安全。
🔬 并发聚集的线程安满是怎么实现的?
在进入详细聚集之前,先聊聊实现原理,这会帮你更好地明白为什么要选这个而不是谁人。
并发聚集重要用了三种技能:
1. 细粒度锁(Fine-grained Locking)
不是"一把大锁锁住整个聚集",而是把聚集分成多少段(Segment)或桶(Bucket),每段一把锁。- ConcurrentDictionary 内部结构(简化):
- _buckets 数组(每个 bucket 是链表头,哈希冲突时向后追加节点):
- ┌───────────────┬───────────────┬───────────────┬───────────────┐
- │ Bucket[0] │ Bucket[1] │ Bucket[2] │ Bucket[3] │ ...
- │ keyA→keyE→… │ keyB │ keyC→keyF→… │ keyD │
- └───────────────┴───────────────┴───────────────┴───────────────┘
- │ │ │ │
- └───────┬───────┘ └───────┬───────┘
- ↓ ↓
- _locks[0] _locks[1] ...
- 映射规则:lock 索引 = bucketIndex % locks.Length
- → Bucket[0]、Bucket[1] 共用 _locks[0]
- → Bucket[2]、Bucket[3] 共用 _locks[1]
- 线程A 写 keyA(落在 Bucket[0])→ 只锁 _locks[0]
- 线程B 写 keyC(落在 Bucket[2])→ 只锁 _locks[1]
- ← 两个线程可以并行!读操作完全不加锁!
复制代码 ConcurrentDictionary 内部默认用 concurrencyLevel(通常便是 CPU 核心数)个锁来分段掩护,大幅镌汰锁竞争。
2. 无锁算法(Lock-free)+ CAS(Compare-And-Swap)
ConcurrentQueue 就是范例的无锁实现。它依靠 CPU 提供的原子指令——Interlocked.CompareExchange(即 CAS):- // CAS 的语义(伪代码):
- if (当前值 == 预期值)
- {
- 当前值 = 新值;
- return true;
- }
- else
- {
- return false; // 有人抢先修改了,重试
- }
复制代码 这个使用在硬件层面是原子的,不须要使用体系内核到场,以是比锁快得多。ConcurrentQueue 用分段数组 + CAS 来管理队头队尾,入队/出队都不消加锁。
假如你对 CAS 和 Interlocked 感爱好,后续在第14篇(低级内存模子)中会深入解说。
3. 不可变 + 写时复制(Immutable + Copy-on-Write)
ImmutableList、ImmutableDictionary 这类不可变聚集天然线程安全——由于它们根本不答应修改。每次"修改"都返回一个新的聚集对象,原来的对象保持稳定,全部线程都可以安全地读取。
📦 Part 1:并发聚集各人族
1.1 ConcurrentDictionary
这是最常用的并发聚集,没有之一。假如你的多线程代码须要共享字典,第一个应该想到的就是它。
根本用法
- var dict = new ConcurrentDictionary<string, int>();
- // 添加
- dict.TryAdd("apple", 1);
- // 获取(不存在时添加)
- int count = dict.GetOrAdd("banana", 0);
- // 更新(原子地读-改-写)
- dict.AddOrUpdate(
- key: "apple",
- addValue: 1,
- updateValueFactory: (key, oldValue) => oldValue + 1
- );
- // 条件更新(仅当值等于预期值时才更新)
- dict.TryUpdate("apple", newValue: 3, comparisonValue: 2);
- // 删除
- dict.TryRemove("banana", out int removed);
复制代码 ⚠️ 最大的坑:非原子的复合使用
ConcurrentDictionary 的每个单独方法是原子的,但方法之间的组合不是!- // ❌ 错误写法:这两步之间可能被其他线程打断
- if (!dict.ContainsKey("key"))
- {
- dict.TryAdd("key", 1);
- }
- // ✅ 正确写法:GetOrAdd 是原子操作
- dict.GetOrAdd("key", 1);
复制代码- // ❌ 错误写法:读-改-写不是原子的
- var old = dict["key"];
- dict["key"] = old + 1;
- // ✅ 正确写法:AddOrUpdate 保证原子性
- dict.AddOrUpdate("key", 1, (k, v) => v + 1);
复制代码 📊 和 Dictionary + lock 的性能对比
在高并发读多写少的场景下,ConcurrentDictionary 比 Dictionary + lock 快许多。在 .NET Core 版本中,读使用是完全无锁的——内部通过 volatile 读取节点引用来包管可见性,不须要获取任何锁,多线程并发读可以完全并行。写使用才会用分段锁,且只锁住目标 bucket 对应的那一把锁。
.NET Framework 版本的 ConcurrentDictionary 读使用仍需短暂持锁,这是两个版天性能差距的重要泉源之一。假如你在 Framework 项目里以为 ConcurrentDictionary 读性能不敷理想,这就是缘故原由。
但在单线程场景下,ConcurrentDictionary 比平凡 Dictionary 要慢一些(由于它有额外的原子使用开销)。以是:
规则:只在真正多线程共享的场景使用 ConcurrentDictionary,单线程就老诚实实用 Dictionary。
实际应用场景
- // 场景一:缓存(多线程读,偶尔写)
- var cache = new ConcurrentDictionary<string, UserInfo>();
- // 场景二:统计计数(多线程并发递增)
- var counters = new ConcurrentDictionary<string, long>();
- counters.AddOrUpdate("pageView", 1, (k, v) => v + 1);
- // 场景三:防重(同一Key只处理一次)
- var processing = new ConcurrentDictionary<Guid, bool>();
- if (processing.TryAdd(orderId, true))
- {
- // 只有一个线程能进来
- await ProcessOrderAsync(orderId);
- processing.TryRemove(orderId, out _);
- }
复制代码 1.2 ConcurrentQueue 和 ConcurrentStack
这两个是无锁实现的队列和栈,非常适当"一端进、一端出"的场景。
ConcurrentQueue:先辈先出(FIFO)
- var queue = new ConcurrentQueue<string>();
- // 入队
- queue.Enqueue("task1");
- queue.Enqueue("task2");
- // 出队(可能失败,因为队列可能是空的)
- if (queue.TryDequeue(out string? item))
- {
- Console.WriteLine($"处理: {item}");
- }
- // 只看不取(peek)
- if (queue.TryPeek(out string? first))
- {
- Console.WriteLine($"下一个是: {first}");
- }
复制代码 ConcurrentStack:后进先出(LIFO)
- var stack = new ConcurrentStack<int>();
- // 入栈(支持批量)
- stack.Push(1);
- stack.PushRange([2, 3, 4]);
- // 出栈
- if (stack.TryPop(out int top))
- {
- Console.WriteLine($"弹出: {top}"); // 4
- }
- // 批量出栈
- int[] batch = new int[3];
- int count = stack.TryPopRange(batch);
复制代码 内部原理
ConcurrentQueue 内部使用分段数组(Segmented Array)+ CAS 的筹划。.NET Framework 版本用的是链表节点 + CAS——每个元素都是一个独立的链表节点,内存不一连,CPU 缓存掷中率低。.NET Core 2.0 对此举行了巨大重写,改为固定巨细(32槽)的数组段,段之间用链表毗连,如许同一个段内的元素在内存上是一连的,大幅提升了缓存局部性(Cache Locality),高并发吞吐量有显着改善:- ConcurrentQueue 内部结构(.NET Core / .NET 5+,简化):
- Segment1[32] → Segment2[32] → Segment3[32]
- ↑ ↑
- _head _tail
- 每个 Segment 是固定 32 槽的数组,槽内元素内存连续,缓存友好
- 入队:向 _tail 所在 Segment 的下一槽写入,满了就 CAS 追加新 Segment
- 出队:从 _head 所在 Segment 读取,空了就 CAS 移动 _head 到下一 Segment
复制代码 全部的 head/tail 指针移动都是用 Interlocked.CompareExchange 完成的,整个过程没有一把锁,这就是它在高并发下性能良好的缘故原由。
1.3 ConcurrentBag:无序并发包
ConcurrentBag 是个很特别的聚集——它不包管序次,读出来的元素序次是不确定的。听起来有点希奇,但它有一个非常适当的场景:"自产自销"模式,即添加和读取使用由同一个线程实验。
内部优化:线程当地存储
ConcurrentBag 内部为每个线程维护了一个当地双端链表(Thread-Local Deque):- 线程A 的本地链表: [A1] ↔ [A2] ↔ [A3] ← 线程A 从头部 Add / TryTake
- 线程B 的本地链表: [B1] ↔ [B2] ← 线程B 从头部 Add / TryTake
- 线程C 的本地链表: [C1]
- 当线程B 本地列表为空时:从线程A 的链表【尾部】偷取 → work-stealing
复制代码 当线程 A 向本身的当地链表 Add 时,绝大多数情况下只须要使用头部,开销极低。当线程 A TryTake 时,也优先从本身的头部取,无需跨线程竞争。只有当当地链表为空须要 work-stealing 时,才会对目标线程的链表加锁(锁的是尾部,而宿主线程使用头部,两端互不干扰,辩说概率很低)。
.NET Framework 版本的 ConcurrentBag 实现相对简朴,work-stealing 的加锁粒度更粗。.NET Core 对其举行了重写,引入了双端链表筹划,使得 work-stealing 对宿主线程的干扰降到最低。
- var bag = new ConcurrentBag<int>();
- // 最适合的场景:多个线程各自产生数据,各自消费
- Parallel.For(0, 100, i =>
- {
- bag.Add(i * i); // 添加到本线程本地列表,无竞争
- bag.TryTake(out _); // 先从本线程本地列表取,无竞争
- });
复制代码 ⚠️ 什么时间不应用 ConcurrentBag
- // ❌ 生产者和消费者是不同线程 → ConcurrentBag 性能很差
- // 应该用 ConcurrentQueue 或 Channel
- var bag = new ConcurrentBag<WorkItem>();
- // 生产线程
- Task.Run(() => {
- while (true) bag.Add(GetNextWork()); // 存到线程1的本地列表
- });
- // 消费线程
- Task.Run(() => {
- while (bag.TryTake(out var item)) // 线程2本地列表是空的,一直在"偷"
- Process(item); // 大量锁竞争!
- });
复制代码 1.4 BlockingCollection:有界限的壅闭聚集
BlockingCollection 是一个包装器,可以把 ConcurrentQueue、ConcurrentStack、ConcurrentBag 包装成一个支持壅闭期待和容量限定的聚集。
它是经典的生产者-斲丧者模式实现,在 Channel 出现之前,这是最主流的做法。- // 创建一个容量上限为 100 的有界队列
- var bc = new BlockingCollection<string>(boundedCapacity: 100);
- // 生产者线程
- var producer = Task.Run(() =>
- {
- for (int i = 0; i < 1000; i++)
- {
- bc.Add($"item-{i}"); // 队列满了会阻塞,直到有位置
- }
- bc.CompleteAdding(); // ← 重要!告诉消费者"没有更多数据了"
- });
- // 消费者线程
- var consumer = Task.Run(() =>
- {
- // GetConsumingEnumerable:自动阻塞等待,直到 CompleteAdding 被调用且集合为空
- foreach (var item in bc.GetConsumingEnumerable())
- {
- Console.WriteLine($"消费: {item}");
- }
- });
- await Task.WhenAll(producer, consumer);
复制代码 关键特性
特性阐明有界限定new BlockingCollection(capacity) 限定最大容量生产者壅闭队列满时,Add 会壅闭(或用 TryAdd + 超时)斲丧者壅闭队列空时,Take 会壅闭(或用 TryTake + 超时)优雅关闭CompleteAdding() 关照斲丧者不会再有新数据取消支持Add/Take 担当 CancellationTokenBlockingCollection 本质上是同步壅闭的。在 async/await 异步代码中,壅闭期待会浪费线程。假如你的代码是异步的,用下面讲的 Channel 才是精确选择。
🚀 Part 2:Channel——当代生产者-斲丧者的首选
Channel(位于 System.Threading.Channels,.NET Core 3.0+ 内置)是专为异步生产者-斲丧者模式筹划的,可以看作是 BlockingCollection 的异步升级版。
为什么要有 Channel?
BlockingCollection 虽好,但它的 Take() 是同步壅闭的——期待时会占用一个线程什么都不干。在高并发的异步体系里,这是一种浪费。
Channel 办理的就是这个标题:期待时不壅闭线程,而是挂起异步使用(await),等有数据了再被叫醒。
创建 Channel
- using System.Threading.Channels;
- // 无界 Channel:生产者永远不会阻塞(但要注意内存)
- var unbounded = Channel.CreateUnbounded<string>();
- // 有界 Channel:超出容量时,行为可配置
- var bounded = Channel.CreateBounded<string>(new BoundedChannelOptions(capacity: 100)
- {
- // 队列满时的策略:
- // Wait = 等待(生产者挂起,默认)
- // DropWrite = 丢弃新写入
- // DropOldest = 丢弃最旧的
- FullMode = BoundedChannelFullMode.Wait,
- // 单生产者/单消费者模式可以开启,性能更好
- SingleWriter = false,
- SingleReader = false
- });
复制代码 根本用法
- var channel = Channel.CreateBounded<WorkItem>(100);
- var writer = channel.Writer;
- var reader = channel.Reader;
- // 生产者(异步写入)
- Exception? fault = null;
- try
- {
- await writer.WriteAsync(new WorkItem("task1")); // 满了则等待
- writer.TryWrite(new WorkItem("task2")); // 满了则返回 false,不等待
- }
- catch (Exception ex)
- {
- fault = ex;
- throw;
- }
- finally
- {
- // ✅ 无论正常还是异常退出,Complete 一定被调用,消费者不会永久挂起
- // fault=null 时等同于 Complete();有异常时把异常传递给消费者
- writer.TryComplete(fault);
- }
- // 消费者(异步读取)
- try
- {
- await foreach (var item in reader.ReadAllAsync()) // 自动等待,直到 Writer Complete
- {
- await ProcessAsync(item);
- }
- }
- catch (ChannelClosedException ex) when (ex.InnerException is not null)
- {
- // 生产者通过 TryComplete(fault) 传来的异常,在此处理
- }
复制代码 多生产者、多斲丧者示例
- var channel = Channel.CreateBounded<string>(50);
- // 多个生产者并发写入
- var producers = Enumerable.Range(0, 3).Select(i => Task.Run(async () =>
- {
- for (int j = 0; j < 20; j++)
- {
- await channel.Writer.WriteAsync($"P{i}-item{j}");
- await Task.Delay(10);
- }
- })).ToArray();
- // ✅ 用 try/finally 协调多生产者的 Complete,不用 fire-and-forget 的 ContinueWith
- // ContinueWith 的异常默认会被吸掉,且时序不可控;try/finally 明确保证任何情况下 Complete 一定被调用
- _ = Task.Run(async () =>
- {
- Exception? fault = null;
- try
- {
- await Task.WhenAll(producers);
- }
- catch (Exception ex)
- {
- fault = ex;
- }
- finally
- {
- channel.Writer.TryComplete(fault);
- }
- });
- // 多个消费者并发读取
- var consumers = Enumerable.Range(0, 2).Select(i => Task.Run(async () =>
- {
- await foreach (var item in channel.Reader.ReadAllAsync())
- {
- Console.WriteLine($"Consumer{i} 处理: {item}");
- }
- })).ToArray();
- await Task.WhenAll(consumers);
复制代码 Channel vs BlockingCollection 选谁?
对比项BlockingCollectionChannel期待方式同步壅闭,占用线程异步挂起,不占线程适当场景同步代码、旧项目异步代码、新项目背压(Backpressure)有界壅闭有界 + 多种计谋完成关照CompleteAdding()Complete()遍历方式GetConsumingEnumerable()ReadAllAsync()性能较好更好(异步友爱)假如你在写新的异步代码,优先选 Channel,它是 .NET 对生产者-斲丧者模式最当代的答案。
🧊 Part 3:不可变聚集(ImmutableCollections)
不可变聚集在 System.Collections.Immutable 定名空间下(NuGet 包:System.Collections.Immutable,.NET 5+ 已内置)。
为什么不可变聚集是线程安全的?
答案极其简朴:由于它们根本不能被修改。
线程安全标题的根源是多个线程对同一块内存举行竞争性的读写。假如一个对象创建后永久不会变,那么多少线程同时读都不会有标题——根本不存在"写"的那一方,竞争消散了。- var list = ImmutableList.Create(1, 2, 3);
- // 所有"修改"操作都返回一个新对象,原来的 list 不变
- var list2 = list.Add(4); // list 还是 [1,2,3],list2 是 [1,2,3,4]
- var list3 = list2.Remove(2); // list2 不变,list3 是 [1,3,4]
- var list4 = list.SetItem(0, 99); // list4 是 [99,2,3],list 不变
- Console.WriteLine(string.Join(",", list)); // 1,2,3(没变!)
- Console.WriteLine(string.Join(",", list2)); // 1,2,3,4
复制代码 常用不可变聚集
- // ImmutableList<T>
- var immutableList = ImmutableList<string>.Empty
- .Add("a")
- .Add("b")
- .Add("c");
- // ImmutableDictionary<TKey, TValue>
- var immutableDict = ImmutableDictionary<string, int>.Empty
- .Add("one", 1)
- .Add("two", 2);
- int val = immutableDict["one"]; // 读取,线程安全
- // ImmutableHashSet<T>
- var immutableSet = ImmutableHashSet.Create("a", "b", "c");
- // ImmutableArray<T>(结构体,零额外内存开销)
- var immutableArr = ImmutableArray.Create(1, 2, 3, 4, 5);
复制代码 内部实现:结构共享(Structural Sharing)
你大概会担心:每次"修改"都创建新对象,内存开销不会很大吗?
不可变集实用了一个非常聪明的技能叫结构共享(Structural Sharing)。以 ImmutableList 为例,它内部用的是AVL 平衡树(二叉搜索树),而不是数组:- 原始列表 [1, 2, 3, 4, 5] 的树结构:
- 3
- / \
- 2 5
- / /
- 1 4
- 执行 Add(6) 后,新叶子 6 的路径是 3 → 5 → 6,路径上的节点需要创建新副本(因为子指针变了),路径之外的节点直接共享:
- 3' ← 新建(右子指针指向新的 5')
- / \
- 2 5' ← 新建(右子指针指向新的 6)
- / / \
- 1 4 6 ← 新建叶子
- 节点 1、2、4 继续被新旧两个版本共享,没有复制!
复制代码 这就是为什么 ImmutableList 的使用是 O(log n) 而不是 O(n)——不须要复制整个列表。
适当哪些场景?
- // 场景一:配置数据(初始化后不变,多线程读取)
- public static readonly ImmutableDictionary<string, string> AppConfig =
- ImmutableDictionary.Create<string, string>()
- .Add("host", "localhost")
- .Add("port", "5432");
- // 场景二:多版本并发控制(MVCC)
- private volatile ImmutableList<LogEntry> _logs = ImmutableList<LogEntry>.Empty;
- public void AddLog(LogEntry entry)
- {
- // 用 Interlocked 原子替换引用,实现无锁更新
- ImmutableList<LogEntry> original, updated;
- do
- {
- original = _logs;
- updated = original.Add(entry);
- } while (Interlocked.CompareExchange(ref _logs, updated, original) != original);
- }
- // 任何线程随时读取 _logs,都拿到一个完整快照,不会读到中间状态
- public ImmutableList<LogEntry> GetLogs() => _logs;
复制代码 ⚠️ ImmutableList vs ImmutableArray
对比项ImmutableListImmutableArray内部结构AVL 树平凡数组随机访问O(log n)O(1)添加/删除O(log n)O(n),需完备复制内存树节点有额外开销零额外开销(结构体)适当场景频仍修改一次创建,只读📊 综合性能对比
理论归理论,终极照旧要在你的详细场景下测试。下面是一些履历性的对比参考:
Dictionary 相干
方案并发写并发读混淆读写Dictionary + lock低(全锁)低(全锁)低Dictionary + ReaderWriterLockSlim中高(读并发)中高ConcurrentDictionary高(分段锁)极高(无锁读)极高ImmutableDictionary + atomic swap极高(写慢,O(log n))极高(完全无锁)视读写比队列相干
方案生产者期待斲丧者期待背压异步友爱ConcurrentQueue无壅闭轮询(busy-wait)无否BlockingCollection同步壅闭同步壅闭有否Channel异步期待异步期待有(可设置)是🛠️ 实战示例:用 Channel 构建日记管道
来一个完备的实战例子,把 Channel 运用到实际的日记体系中:- // 见示例代码:ConcurrentCollections 项目
复制代码 (完备代码见本章配套项目)
📝 本章总结
聚集内部机制适当场景ConcurrentDictionary分段锁 + 部门无锁读多线程共享字典、缓存、计数器ConcurrentQueue无锁 CAS + 分段数组线程安全 FIFO 队列,轻量生产斲丧ConcurrentStack无锁 CAS + 链表线程安全 LIFO 栈ConcurrentBag线程当地列表 + work-stealing同线程"自产自销"BlockingCollection包装器(同步壅闭)同步代码的有界生产斲丧Channel异步信号量 + 无锁队列异步代码的生产斲丧,首选ImmutableXxx恒久化数据结构(树/数组)只读共享、设置、快照一句话版本:
- 多线程共享字典 → ConcurrentDictionary
- 异步生产斲丧 → Channel
- 同步生产斲丧 → BlockingCollection
- 自产自销 → ConcurrentBag
- 只读共享 → Immutable 系列
- 不知道用啥 → 先翻第11篇复习锁,想清晰再选 😄
💡 快速参考卡片
- ┌─────────────────────────────────────────────────────────────────┐
- │ 并发集合选择指南 │
- ├─────────────────────────────────────────────────────────────────┤
- │ 需要字典? │
- │ └─ 多线程 → ConcurrentDictionary │
- │ └─ 单线程 → Dictionary(更快) │
- │ │
- │ 需要队列/生产消费? │
- │ └─ 异步代码 → Channel<T>(首选) │
- │ └─ 同步代码 → BlockingCollection<T> │
- │ └─ 无需等待/无界 → ConcurrentQueue<T> │
- │ │
- │ 需要栈? → ConcurrentStack<T> │
- │ │
- │ 同线程自产自销? → ConcurrentBag<T> │
- │ │
- │ 只读共享/配置? → ImmutableXxx<T> │
- └─────────────────────────────────────────────────────────────────┘
复制代码 🔗 参考资源
- Microsoft Docs:System.Collections.Concurrent
- Microsoft Docs:System.Threading.Channels
- System.Collections.Immutable
- Channel 深度分析 - Stephen Toub
- ConcurrentDictionary 源码(.NET Runtime)
|