【.NET并发编程 - 12】并发聚集与线程安全范例

[复制链接]
发表于 2026-5-31 04:08:18 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
12. 并发聚集与线程安全范例

本章 GitHub 堆栈:csharp-concurrency-cookbook ⭐
欢迎 Star 和 Fork!全部代码示例都可以在堆栈中找到并运行。
🎯 本章导读

📌 本文目标:搞清晰 System.Collections.Concurrent 定名空间里那些并发聚集的精确用法和内部原理,相识 Channel 这个当代生产者-斲丧者利器,以及不可变聚集(ImmutableCollections)的线程安全之道。
上一篇中,我们把 C# 里的各种锁摸了个遍——从 lock 到 SemaphoreSlim,从 SpinLock 到 AsyncLock。但是,大概有人问这个标题:
"我只是想在多线程里维护一个字典 / 队列,岂非每次我都要本身加锁吗?"
答案固然是:不消。.NET 早就替你把这部门活儿干了,这就是本日要聊的——并发聚集。
不外,并发聚集不是银弹。它们各自有适当的场景,也有许多初学者踩过的坑。本日我们就体系地把这张牌打开,让你对每一张都心田有数。
🤔 先想想:为什么平凡 List/Dictionary 不是线程安全的?

这是个好标题,许多人用了好久并发聚集,却从没认真想过这一层。
以 List.Add 为例

我们来看看 List 内部大概长什么样(简化版):
  1. // List<T> 内部核心字段(简化)
  2. private T[] _items;    // 底层数组
  3. private int _size;     // 当前元素个数
  4. public void Add(T item)
  5. {
  6.         // 第一步:容量不够时扩容(翻倍策略,创建新数组并复制旧数据)
  7.         if (_size == _items.Length)
  8.                 EnsureCapacity();
  9.         // 第二步:写入数据
  10.         _items[_size] = item;
  11.         // 第三步:更新计数
  12.         _size++;
  13. }
复制代码
现在想象两个线程同时调用 Add,_size 当前是 5:
  1. 线程A:读取 _size = 5
  2. 线程B:读取 _size = 5
  3. 线程A:_items[5] = "A"
  4. 线程B:_items[5] = "B"  ← 覆盖了线程A写的!
  5. 线程A:_size = 6
  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),每段一把锁。
  1. ConcurrentDictionary 内部结构(简化):
  2.   _buckets 数组(每个 bucket 是链表头,哈希冲突时向后追加节点):
  3.   ┌───────────────┬───────────────┬───────────────┬───────────────┐
  4.   │  Bucket[0]    │  Bucket[1]    │  Bucket[2]    │  Bucket[3]    │ ...
  5.   │ keyA→keyE→…   │   keyB        │ keyC→keyF→…   │   keyD        │
  6.   └───────────────┴───────────────┴───────────────┴───────────────┘
  7.           │               │               │               │
  8.           └───────┬───────┘               └───────┬───────┘
  9.                   ↓                               ↓
  10.               _locks[0]                        _locks[1]           ...
  11.   映射规则:lock 索引 = bucketIndex % locks.Length
  12.   → Bucket[0]、Bucket[1] 共用 _locks[0]
  13.   → Bucket[2]、Bucket[3] 共用 _locks[1]
  14. 线程A 写 keyA(落在 Bucket[0])→ 只锁 _locks[0]
  15. 线程B 写 keyC(落在 Bucket[2])→ 只锁 _locks[1]
  16. ← 两个线程可以并行!读操作完全不加锁!
复制代码
ConcurrentDictionary 内部默认用 concurrencyLevel(通常便是 CPU 核心数)个锁来分段掩护,大幅镌汰锁竞争。
2. 无锁算法(Lock-free)+ CAS(Compare-And-Swap)

ConcurrentQueue 就是范例的无锁实现。它依靠 CPU 提供的原子指令——Interlocked.CompareExchange(即 CAS):
  1. // CAS 的语义(伪代码):
  2. if (当前值 == 预期值)
  3. {
  4.         当前值 = 新值;
  5.         return true;
  6. }
  7. else
  8. {
  9.         return false; // 有人抢先修改了,重试
  10. }
复制代码
这个使用在硬件层面是原子的,不须要使用体系内核到场,以是比锁快得多。ConcurrentQueue 用分段数组 + CAS 来管理队头队尾,入队/出队都不消加锁。
假如你对 CAS 和 Interlocked 感爱好,后续在第14篇(低级内存模子)中会深入解说。
3. 不可变 + 写时复制(Immutable + Copy-on-Write)

ImmutableList、ImmutableDictionary 这类不可变聚集天然线程安全——由于它们根本不答应修改。每次"修改"都返回一个新的聚集对象,原来的对象保持稳定,全部线程都可以安全地读取。
📦 Part 1:并发聚集各人族

1.1 ConcurrentDictionary

这是最常用的并发聚集,没有之一。假如你的多线程代码须要共享字典,第一个应该想到的就是它。
根本用法
  1. var dict = new ConcurrentDictionary<string, int>();
  2. // 添加
  3. dict.TryAdd("apple", 1);
  4. // 获取(不存在时添加)
  5. int count = dict.GetOrAdd("banana", 0);
  6. // 更新(原子地读-改-写)
  7. dict.AddOrUpdate(
  8.         key: "apple",
  9.         addValue: 1,
  10.         updateValueFactory: (key, oldValue) => oldValue + 1
  11. );
  12. // 条件更新(仅当值等于预期值时才更新)
  13. dict.TryUpdate("apple", newValue: 3, comparisonValue: 2);
  14. // 删除
  15. dict.TryRemove("banana", out int removed);
复制代码
⚠️ 最大的坑:非原子的复合使用

ConcurrentDictionary 的每个单独方法是原子的,但方法之间的组合不是!
  1. // ❌ 错误写法:这两步之间可能被其他线程打断
  2. if (!dict.ContainsKey("key"))
  3. {
  4.         dict.TryAdd("key", 1);
  5. }
  6. // ✅ 正确写法:GetOrAdd 是原子操作
  7. dict.GetOrAdd("key", 1);
复制代码
  1. // ❌ 错误写法:读-改-写不是原子的
  2. var old = dict["key"];
  3. dict["key"] = old + 1;
  4. // ✅ 正确写法:AddOrUpdate 保证原子性
  5. 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。
实际应用场景
  1. // 场景一:缓存(多线程读,偶尔写)
  2. var cache = new ConcurrentDictionary<string, UserInfo>();
  3. // 场景二:统计计数(多线程并发递增)
  4. var counters = new ConcurrentDictionary<string, long>();
  5. counters.AddOrUpdate("pageView", 1, (k, v) => v + 1);
  6. // 场景三:防重(同一Key只处理一次)
  7. var processing = new ConcurrentDictionary<Guid, bool>();
  8. if (processing.TryAdd(orderId, true))
  9. {
  10.         // 只有一个线程能进来
  11.         await ProcessOrderAsync(orderId);
  12.         processing.TryRemove(orderId, out _);
  13. }
复制代码
1.2 ConcurrentQueue 和 ConcurrentStack

这两个是无锁实现的队列和栈,非常适当"一端进、一端出"的场景。
ConcurrentQueue:先辈先出(FIFO)
  1. var queue = new ConcurrentQueue<string>();
  2. // 入队
  3. queue.Enqueue("task1");
  4. queue.Enqueue("task2");
  5. // 出队(可能失败,因为队列可能是空的)
  6. if (queue.TryDequeue(out string? item))
  7. {
  8.         Console.WriteLine($"处理: {item}");
  9. }
  10. // 只看不取(peek)
  11. if (queue.TryPeek(out string? first))
  12. {
  13.         Console.WriteLine($"下一个是: {first}");
  14. }
复制代码
ConcurrentStack:后进先出(LIFO)
  1. var stack = new ConcurrentStack<int>();
  2. // 入栈(支持批量)
  3. stack.Push(1);
  4. stack.PushRange([2, 3, 4]);
  5. // 出栈
  6. if (stack.TryPop(out int top))
  7. {
  8.         Console.WriteLine($"弹出: {top}"); // 4
  9. }
  10. // 批量出栈
  11. int[] batch = new int[3];
  12. int count = stack.TryPopRange(batch);
复制代码
内部原理

ConcurrentQueue 内部使用分段数组(Segmented Array)+ CAS 的筹划。.NET Framework 版本用的是链表节点 + CAS——每个元素都是一个独立的链表节点,内存不一连,CPU 缓存掷中率低。.NET Core 2.0 对此举行了巨大重写,改为固定巨细(32槽)的数组段,段之间用链表毗连,如许同一个段内的元素在内存上是一连的,大幅提升了缓存局部性(Cache Locality),高并发吞吐量有显着改善:
  1. ConcurrentQueue 内部结构(.NET Core / .NET 5+,简化):
  2.   Segment1[32] → Segment2[32] → Segment3[32]
  3.          ↑                                  ↑
  4.    _head                             _tail
  5. 每个 Segment 是固定 32 槽的数组,槽内元素内存连续,缓存友好
  6. 入队:向 _tail 所在 Segment 的下一槽写入,满了就 CAS 追加新 Segment
  7. 出队:从 _head 所在 Segment 读取,空了就 CAS 移动 _head 到下一 Segment
复制代码
全部的 head/tail 指针移动都是用 Interlocked.CompareExchange 完成的,整个过程没有一把锁,这就是它在高并发下性能良好的缘故原由。
1.3 ConcurrentBag:无序并发包

ConcurrentBag 是个很特别的聚集——它不包管序次,读出来的元素序次是不确定的。听起来有点希奇,但它有一个非常适当的场景:"自产自销"模式,即添加和读取使用由同一个线程实验。
内部优化:线程当地存储

ConcurrentBag 内部为每个线程维护了一个当地双端链表(Thread-Local Deque):
  1. 线程A 的本地链表: [A1] ↔ [A2] ↔ [A3]   ← 线程A 从头部 Add / TryTake
  2. 线程B 的本地链表: [B1] ↔ [B2]            ← 线程B 从头部 Add / TryTake
  3. 线程C 的本地链表: [C1]
  4. 当线程B 本地列表为空时:从线程A 的链表【尾部】偷取 → work-stealing
复制代码
当线程 A 向本身的当地链表 Add 时,绝大多数情况下只须要使用头部,开销极低。当线程 A TryTake 时,也优先从本身的头部取,无需跨线程竞争。只有当当地链表为空须要 work-stealing 时,才会对目标线程的链表加锁(锁的是尾部,而宿主线程使用头部,两端互不干扰,辩说概率很低)。
.NET Framework 版本的 ConcurrentBag 实现相对简朴,work-stealing 的加锁粒度更粗。.NET Core 对其举行了重写,引入了双端链表筹划,使得 work-stealing 对宿主线程的干扰降到最低。
  1. var bag = new ConcurrentBag<int>();
  2. // 最适合的场景:多个线程各自产生数据,各自消费
  3. Parallel.For(0, 100, i =>
  4. {
  5.         bag.Add(i * i);      // 添加到本线程本地列表,无竞争
  6.         bag.TryTake(out _);  // 先从本线程本地列表取,无竞争
  7. });
复制代码
⚠️ 什么时间不应用 ConcurrentBag
  1. // ❌ 生产者和消费者是不同线程 → ConcurrentBag 性能很差
  2. // 应该用 ConcurrentQueue 或 Channel
  3. var bag = new ConcurrentBag<WorkItem>();
  4. // 生产线程
  5. Task.Run(() => {
  6.         while (true) bag.Add(GetNextWork()); // 存到线程1的本地列表
  7. });
  8. // 消费线程
  9. Task.Run(() => {
  10.         while (bag.TryTake(out var item)) // 线程2本地列表是空的,一直在"偷"
  11.                 Process(item);                 // 大量锁竞争!
  12. });
复制代码
1.4 BlockingCollection:有界限的壅闭聚集

BlockingCollection 是一个包装器,可以把 ConcurrentQueue、ConcurrentStack、ConcurrentBag 包装成一个支持壅闭期待和容量限定的聚集。
它是经典的生产者-斲丧者模式实现,在 Channel 出现之前,这是最主流的做法。
  1. // 创建一个容量上限为 100 的有界队列
  2. var bc = new BlockingCollection<string>(boundedCapacity: 100);
  3. // 生产者线程
  4. var producer = Task.Run(() =>
  5. {
  6.         for (int i = 0; i < 1000; i++)
  7.         {
  8.                 bc.Add($"item-{i}"); // 队列满了会阻塞,直到有位置
  9.         }
  10.         bc.CompleteAdding(); // ← 重要!告诉消费者"没有更多数据了"
  11. });
  12. // 消费者线程
  13. var consumer = Task.Run(() =>
  14. {
  15.         // GetConsumingEnumerable:自动阻塞等待,直到 CompleteAdding 被调用且集合为空
  16.         foreach (var item in bc.GetConsumingEnumerable())
  17.         {
  18.                 Console.WriteLine($"消费: {item}");
  19.         }
  20. });
  21. await Task.WhenAll(producer, consumer);
复制代码
关键特性

特性阐明有界限定new BlockingCollection(capacity) 限定最大容量生产者壅闭队列满时,Add 会壅闭(或用 TryAdd + 超时)斲丧者壅闭队列空时,Take 会壅闭(或用 TryTake + 超时)优雅关闭CompleteAdding() 关照斲丧者不会再有新数据取消支持Add/Take 担当 CancellationToken
BlockingCollection 本质上是同步壅闭的。在 async/await 异步代码中,壅闭期待会浪费线程。假如你的代码是异步的,用下面讲的 Channel 才是精确选择。
🚀 Part 2:Channel——当代生产者-斲丧者的首选

Channel(位于 System.Threading.Channels,.NET Core 3.0+ 内置)是专为异步生产者-斲丧者模式筹划的,可以看作是 BlockingCollection 的异步升级版。
为什么要有 Channel?

BlockingCollection 虽好,但它的 Take() 是同步壅闭的——期待时会占用一个线程什么都不干。在高并发的异步体系里,这是一种浪费。
Channel 办理的就是这个标题:期待时不壅闭线程,而是挂起异步使用(await),等有数据了再被叫醒。
创建 Channel
  1. using System.Threading.Channels;
  2. // 无界 Channel:生产者永远不会阻塞(但要注意内存)
  3. var unbounded = Channel.CreateUnbounded<string>();
  4. // 有界 Channel:超出容量时,行为可配置
  5. var bounded = Channel.CreateBounded<string>(new BoundedChannelOptions(capacity: 100)
  6. {
  7.         // 队列满时的策略:
  8.         // Wait = 等待(生产者挂起,默认)
  9.         // DropWrite = 丢弃新写入
  10.         // DropOldest = 丢弃最旧的
  11.         FullMode = BoundedChannelFullMode.Wait,
  12.         // 单生产者/单消费者模式可以开启,性能更好
  13.         SingleWriter = false,
  14.         SingleReader = false
  15. });
复制代码
根本用法
  1. var channel = Channel.CreateBounded<WorkItem>(100);
  2. var writer = channel.Writer;
  3. var reader = channel.Reader;
  4. // 生产者(异步写入)
  5. Exception? fault = null;
  6. try
  7. {
  8.         await writer.WriteAsync(new WorkItem("task1")); // 满了则等待
  9.         writer.TryWrite(new WorkItem("task2"));          // 满了则返回 false,不等待
  10. }
  11. catch (Exception ex)
  12. {
  13.         fault = ex;
  14.         throw;
  15. }
  16. finally
  17. {
  18.         // ✅ 无论正常还是异常退出,Complete 一定被调用,消费者不会永久挂起
  19.         // fault=null 时等同于 Complete();有异常时把异常传递给消费者
  20.         writer.TryComplete(fault);
  21. }
  22. // 消费者(异步读取)
  23. try
  24. {
  25.         await foreach (var item in reader.ReadAllAsync()) // 自动等待,直到 Writer Complete
  26.         {
  27.                 await ProcessAsync(item);
  28.         }
  29. }
  30. catch (ChannelClosedException ex) when (ex.InnerException is not null)
  31. {
  32.         // 生产者通过 TryComplete(fault) 传来的异常,在此处理
  33. }
复制代码
多生产者、多斲丧者示例
  1. var channel = Channel.CreateBounded<string>(50);
  2. // 多个生产者并发写入
  3. var producers = Enumerable.Range(0, 3).Select(i => Task.Run(async () =>
  4. {
  5.         for (int j = 0; j < 20; j++)
  6.         {
  7.                 await channel.Writer.WriteAsync($"P{i}-item{j}");
  8.                 await Task.Delay(10);
  9.         }
  10. })).ToArray();
  11. // ✅ 用 try/finally 协调多生产者的 Complete,不用 fire-and-forget 的 ContinueWith
  12. // ContinueWith 的异常默认会被吸掉,且时序不可控;try/finally 明确保证任何情况下 Complete 一定被调用
  13. _ = Task.Run(async () =>
  14. {
  15.         Exception? fault = null;
  16.         try
  17.         {
  18.                 await Task.WhenAll(producers);
  19.         }
  20.         catch (Exception ex)
  21.         {
  22.                 fault = ex;
  23.         }
  24.         finally
  25.         {
  26.                 channel.Writer.TryComplete(fault);
  27.         }
  28. });
  29. // 多个消费者并发读取
  30. var consumers = Enumerable.Range(0, 2).Select(i => Task.Run(async () =>
  31. {
  32.         await foreach (var item in channel.Reader.ReadAllAsync())
  33.         {
  34.                 Console.WriteLine($"Consumer{i} 处理: {item}");
  35.         }
  36. })).ToArray();
  37. 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+ 已内置)。
为什么不可变聚集是线程安全的?

答案极其简朴:由于它们根本不能被修改。
线程安全标题的根源是多个线程对同一块内存举行竞争性的读写。假如一个对象创建后永久不会变,那么多少线程同时读都不会有标题——根本不存在"写"的那一方,竞争消散了。
  1. var list = ImmutableList.Create(1, 2, 3);
  2. // 所有"修改"操作都返回一个新对象,原来的 list 不变
  3. var list2 = list.Add(4);          // list 还是 [1,2,3],list2 是 [1,2,3,4]
  4. var list3 = list2.Remove(2);      // list2 不变,list3 是 [1,3,4]
  5. var list4 = list.SetItem(0, 99);  // list4 是 [99,2,3],list 不变
  6. Console.WriteLine(string.Join(",", list));  // 1,2,3(没变!)
  7. Console.WriteLine(string.Join(",", list2)); // 1,2,3,4
复制代码
常用不可变聚集
  1. // ImmutableList<T>
  2. var immutableList = ImmutableList<string>.Empty
  3.         .Add("a")
  4.         .Add("b")
  5.         .Add("c");
  6. // ImmutableDictionary<TKey, TValue>
  7. var immutableDict = ImmutableDictionary<string, int>.Empty
  8.         .Add("one", 1)
  9.         .Add("two", 2);
  10. int val = immutableDict["one"]; // 读取,线程安全
  11. // ImmutableHashSet<T>
  12. var immutableSet = ImmutableHashSet.Create("a", "b", "c");
  13. // ImmutableArray<T>(结构体,零额外内存开销)
  14. var immutableArr = ImmutableArray.Create(1, 2, 3, 4, 5);
复制代码
内部实现:结构共享(Structural Sharing)

你大概会担心:每次"修改"都创建新对象,内存开销不会很大吗?
不可变集实用了一个非常聪明的技能叫结构共享(Structural Sharing)。以 ImmutableList 为例,它内部用的是AVL 平衡树(二叉搜索树),而不是数组:
  1. 原始列表 [1, 2, 3, 4, 5] 的树结构:
  2.                  3
  3.                 / \
  4.            2   5
  5.           /   /
  6.          1   4
  7. 执行 Add(6) 后,新叶子 6 的路径是 3 → 5 → 6,路径上的节点需要创建新副本(因为子指针变了),路径之外的节点直接共享:
  8.                  3'         ← 新建(右子指针指向新的 5')
  9.                 / \
  10.            2   5'        ← 新建(右子指针指向新的 6)
  11.           /   / \
  12.          1   4   6   ← 新建叶子
  13. 节点 1、2、4 继续被新旧两个版本共享,没有复制!
复制代码
这就是为什么 ImmutableList 的使用是 O(log n) 而不是 O(n)——不须要复制整个列表。
适当哪些场景?
  1. // 场景一:配置数据(初始化后不变,多线程读取)
  2. public static readonly ImmutableDictionary<string, string> AppConfig =
  3.         ImmutableDictionary.Create<string, string>()
  4.                 .Add("host", "localhost")
  5.                 .Add("port", "5432");
  6. // 场景二:多版本并发控制(MVCC)
  7. private volatile ImmutableList<LogEntry> _logs = ImmutableList<LogEntry>.Empty;
  8. public void AddLog(LogEntry entry)
  9. {
  10.         // 用 Interlocked 原子替换引用,实现无锁更新
  11.         ImmutableList<LogEntry> original, updated;
  12.         do
  13.         {
  14.                 original = _logs;
  15.                 updated = original.Add(entry);
  16.         } while (Interlocked.CompareExchange(ref _logs, updated, original) != original);
  17. }
  18. // 任何线程随时读取 _logs,都拿到一个完整快照,不会读到中间状态
  19. 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 运用到实际的日记体系中:
  1. // 见示例代码:ConcurrentCollections 项目
复制代码
(完备代码见本章配套项目)
📝 本章总结

聚集内部机制适当场景ConcurrentDictionary分段锁 + 部门无锁读多线程共享字典、缓存、计数器ConcurrentQueue无锁 CAS + 分段数组线程安全 FIFO 队列,轻量生产斲丧ConcurrentStack无锁 CAS + 链表线程安全 LIFO 栈ConcurrentBag线程当地列表 + work-stealing同线程"自产自销"BlockingCollection包装器(同步壅闭)同步代码的有界生产斲丧Channel异步信号量 + 无锁队列异步代码的生产斲丧,首选ImmutableXxx恒久化数据结构(树/数组)只读共享、设置、快照一句话版本:

  • 多线程共享字典 → ConcurrentDictionary
  • 异步生产斲丧 → Channel
  • 同步生产斲丧 → BlockingCollection
  • 自产自销 → ConcurrentBag
  • 只读共享 → Immutable 系列
  • 不知道用啥 → 先翻第11篇复习锁,想清晰再选 😄
💡 快速参考卡片
  1. ┌─────────────────────────────────────────────────────────────────┐
  2. │                  并发集合选择指南                                  │
  3. ├─────────────────────────────────────────────────────────────────┤
  4. │  需要字典?                                                        │
  5. │    └─ 多线程 → ConcurrentDictionary                               │
  6. │    └─ 单线程 → Dictionary(更快)                                  │
  7. │                                                                   │
  8. │  需要队列/生产消费?                                                │
  9. │    └─ 异步代码 → Channel<T>(首选)                                │
  10. │    └─ 同步代码 → BlockingCollection<T>                            │
  11. │    └─ 无需等待/无界 → ConcurrentQueue<T>                          │
  12. │                                                                   │
  13. │  需要栈? → ConcurrentStack<T>                                    │
  14. │                                                                   │
  15. │  同线程自产自销? → ConcurrentBag<T>                              │
  16. │                                                                   │
  17. │  只读共享/配置? → ImmutableXxx<T>                                │
  18. └─────────────────────────────────────────────────────────────────┘
复制代码
🔗 参考资源


  • Microsoft Docs:System.Collections.Concurrent
  • Microsoft Docs:System.Threading.Channels
  • System.Collections.Immutable
  • Channel 深度分析 - Stephen Toub
  • ConcurrentDictionary 源码(.NET Runtime)
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表