0

0

C#的ConcurrentBag如何实现线程安全集合?

畫卷琴夢

畫卷琴夢

发布时间:2025-08-08 10:42:02

|

638人浏览过

|

来源于php中文网

原创

concurrentbag通过线程局部存储和工作窃取实现线程安全,1. 每个线程优先操作自己的本地“小袋子”,add和take在本地无锁进行;2. 当本地为空时,线程从其他线程的袋子尾部窃取元素,减少冲突;3. 该机制在生产者-消费者同线程、任务无序处理、局部操作频繁的场景下性能最佳;4. 但存在工作窃取开销大、无序性、toarray/clear/contains性能差、内存开销高等局限;5. 与concurrentqueue(fifo)和concurrentstack(lifo)相比,concurrentbag不保证顺序,侧重吞吐量而非顺序一致性,适用于对顺序无要求但需高并发性能的负载均衡或任务池场景。

C#的ConcurrentBag<T>如何实现线程安全集合?

C#中的

ConcurrentBag
实现线程安全集合,其核心在于巧妙地结合了线程局部存储(Thread-Local Storage, TLS)和工作窃取(Work-Stealing)算法。这意味着,当一个线程添加或移除元素时,它会优先操作自己“专属”的局部存储空间,极大地减少了多线程之间的直接竞争,从而达到高效的线程安全。

解决方案

要理解

ConcurrentBag
如何做到线程安全,得深入它那有点“狡猾”的内部机制。它不像
ConcurrentQueue
ConcurrentStack
那样,通常围绕一个共享的、需要精细同步的数据结构打转。
ConcurrentBag
的聪明之处在于它试图避免这种直接竞争。

它为每个线程维护一个私有的、类似列表的“小袋子”(或者说,一个内部的、线程本地的双端队列Deque)。当一个线程调用

Add
方法时,它会将元素添加到自己线程的这个“小袋子”的头部。这个操作几乎是无锁的,因为每个线程都在操作自己的私有数据,互不干扰。这就像每个人都有自己的购物篮,往里面放东西的时候不用排队。

当一个线程需要通过

Take
方法取走一个元素时,它会首先尝试从自己的“小袋子”的头部取走。如果自己的袋子里有东西,那太好了,又是一个无锁操作。但如果自己的袋子空了,问题就来了:它需要找点活干。这时候,它就会尝试去“偷”其他线程袋子里的元素。这个“偷”的过程才是真正涉及到线程同步的地方。它会从其他线程的“小袋子”的尾部去取元素。之所以从尾部取,是为了减少与该线程自身在头部添加/移除时的冲突。这种窃取操作是需要加锁的,但因为是在本地袋子为空时才发生,所以整体上锁的频率和粒度都比直接共享的集合要低得多。

这种设计哲学,我个人觉得非常精妙,它利用了多线程行为的常见模式:线程通常会处理自己产生的数据。只有当一个线程“闲”下来,没有自己的活可干时,它才会去“打扰”别的线程。这种“各扫门前雪,有空再帮人”的策略,是

ConcurrentBag
实现高性能线程安全的关键。

ConcurrentBag
在什么场景下表现最佳?

从我的经验来看,

ConcurrentBag
在某些特定场景下能发挥出令人惊喜的性能优势,甚至超越其他并发集合。

一个非常典型的场景是生产者-消费者模式,尤其是当生产者和消费者是同一个线程,或者说,一个线程倾向于消费自己之前生产的元素时。举个例子,你有一个任务处理系统,每个工作线程会生成一些子任务,并且这些子任务最好由生成它们的线程来处理。如果这个线程处理完了自己的子任务,它才会去帮助其他线程处理它们的子任务。在这种情况下,

ConcurrentBag
的线程局部存储特性就显得尤为高效,因为大部分
Add
Take
操作都发生在线程内部,避免了跨线程的锁竞争。

另一个适合它的场景是任务分发与负载均衡。当你有大量不相关的任务需要并行处理,并且任务的顺序不重要时,

ConcurrentBag
可以作为一个任务池。每个工作线程从这个池中取出任务执行,如果自己的任务队列空了,就去“偷”其他线程的任务。这种设计非常适合那些任务量动态变化、且需要所有CPU核心都尽可能忙碌的计算密集型应用。

它也适用于高并发、但局部竞争较低的场景。如果你的操作模式是大量的

Add
,并且
Take
操作相对较少,或者
Take
操作在大部分时间里都能从线程本地的“小袋子”里取到元素,那么
ConcurrentBag
的性能会非常出色。它将全局锁竞争转化为了局部的、偶尔的竞争,显著提升了吞吐量。

新快购物系统
新快购物系统

新快购物系统是集合目前网络所有购物系统为参考而开发,不管从速度还是安全我们都努力做到最好,此版虽为免费版但是功能齐全,无任何错误,特点有:专业的、全面的电子商务解决方案,使您可以轻松实现网上销售;自助式开放性的数据平台,为您提供充满个性化的设计空间;功能全面、操作简单的远程管理系统,让您在家中也可实现正常销售管理;严谨实用的全新商品数据库,便于查询搜索您的商品。

下载

ConcurrentBag
的性能陷阱和局限性是什么?

尽管

ConcurrentBag
设计巧妙,但它并非银弹,使用不当同样会带来性能问题,甚至可能不如其他并发集合。

首先,工作窃取机制的开销。虽然它旨在减少竞争,但如果你的应用模式导致频繁的工作窃取,比如所有线程都很快清空了自己的本地“小袋子”,然后同时去抢一个繁忙线程的元素,那么这种窃取操作的开销(包括锁竞争和跨线程内存访问)就会变得非常显著,甚至可能导致性能下降。我见过一些案例,当

Take
操作远多于
Add
,且所有线程都在争抢少数几个“富裕”线程的资源时,
ConcurrentBag
的表现反而不如预期。

其次,无序性是一个重要的局限。

ConcurrentBag
不保证任何元素的取出顺序,它既不是FIFO(先进先出),也不是LIFO(后进先出)。你取出的元素很可能是你当前线程自己最后放入的(从本地袋子取),也可能是其他线程放入的某个元素(窃取而来)。如果你的业务逻辑对元素的处理顺序有严格要求,比如消息队列、事件日志等,那么
ConcurrentBag
是绝对不适合的,你可能需要考虑
ConcurrentQueue

再者,某些操作会破坏其性能优势。例如,

ToArray()
Clear()
Contains()
方法
。这些操作需要遍历所有线程的局部“小袋子”,并可能涉及全局锁或复杂的同步机制,因此它们的性能开销通常会非常大。如果你需要频繁地将集合内容转换为数组,或者频繁检查某个元素是否存在,那么
ConcurrentBag
的性能优势将荡然无存,甚至可能成为瓶颈。它更适合作为一个动态的、只关注添加和移除的“工作池”。

最后,内存开销也是一个潜在问题。由于每个线程都可能维护自己的内部存储,如果你的应用程序创建了大量短生命周期的线程,或者线程池中的线程数量非常多,并且每个线程都短暂地使用了

ConcurrentBag
,那么这些分散的内部存储可能会占用更多的内存,并且增加垃圾回收的压力。

ConcurrentBag
ConcurrentQueue
ConcurrentStack
的主要区别是什么?

理解

ConcurrentBag
ConcurrentQueue
ConcurrentStack
之间的差异,是选择正确并发集合的关键。它们虽然都提供线程安全,但在内部机制、性能特点和适用场景上有着根本性的不同。

最核心的区别在于元素顺序的保证

  • ConcurrentQueue
    :严格遵循FIFO(First-In, First-Out)原则,即先进入集合的元素,总是先被取出。它就像一个排队的队伍,谁先来谁先走。这使得它非常适合实现消息队列、任务调度等需要保持严格处理顺序的场景。
  • ConcurrentStack
    :严格遵循LIFO(Last-In, First-Out)原则,即最后进入集合的元素,总是最先被取出。它就像一叠盘子,你总是从最上面取,也总是把新盘子放在最上面。这让它非常适合实现撤销/重做功能、调用堆栈等需要处理最近状态的场景。
  • ConcurrentBag
    不保证任何顺序。你取出的元素可能是你当前线程最后放入的,也可能是从其他线程“偷”来的某个元素。这种无序性是其实现高性能的关键,因为它不需要为了维护顺序而引入复杂的同步机制。

其次是内部实现和性能侧重

  • ConcurrentQueue
    ConcurrentStack
    通常基于共享的、链表或数组结构,通过复杂的无锁算法(如CAS操作)或细粒度锁来保证在多线程访问时的原子性和一致性,它们更侧重于在共享资源上的高效同步。
  • ConcurrentBag
    则如前所述,通过线程局部存储和工作窃取来减少对共享资源的直接竞争。它的设计哲学是“能不共享就不共享,实在要共享再同步”。这使得它在
    Add
    操作和大部分
    Take
    操作上拥有极低的同步开销,特别是在线程倾向于处理自己数据的场景。

最后,它们的适用场景也因此不同。

  • 如果你需要严格的顺序保证(FIFO或LIFO),并且集合中的元素是需要按特定顺序处理的任务或数据,那么
    ConcurrentQueue
    ConcurrentStack
    是你的首选。
  • 如果你对元素的处理顺序没有要求,但希望在多线程环境下最大化吞吐量,减少锁竞争,并且你的线程模式是倾向于处理自己生产的数据,或者能够通过“窃取”来平衡负载,那么
    ConcurrentBag
    会是更优的选择。它更像是一个“任务池”或“物品袋”,只关心有没有东西可取,不关心是哪个线程放的,也不关心是第几个放的。

相关专题

更多
treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

535

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

17

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

17

2026.01.06

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

391

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

572

2023.08.10

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

391

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

572

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

481

2023.08.10

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号