1. 并发集合的概述
在C#语言中当需要处理并发的场景时,就需要程序员使用最合理的数据结构。那么哪些数据结构是支持和可以在并行计算中被使用的呢。首先这些数据结构具备可伸缩性,尽可能地避免锁(会造成多个线程的等待,防止资源竞争),同时还能提供线程安全的访问。
在.NET Framework4.0中引入了System.Collections.Concurrent命名空间,其中就包含几个数据结构。
- ConcurrentQueue:提供线程安全的先进先出集合
- ConcurrentDictionary:提供可有多个线程同时访问的键值对的线程安全集合
- ConcurrentStack:提供线程安全的后进先出集合
- ConcurrentBag:提供对象的线程安全的无序集合
- BlockingCollect:与经典的阻塞队列数据结构类似,能够适用于多个任务添加和删除数据,提供阻塞和限界能力。
.NET Framework 4提供了新的线程安全和扩展的并发集合,它们能够解决潜在的死锁问题和竞争条件问题,因此在很多复杂的情形下它们能够使得并行代码更容易编写,这些集合尽可能减少需要使用锁的次数,从而使得在大部分情形下能够优化为最佳性能,不会产生不必要的同步开销。需要注意的是:
线程安全并不是没有代价的,比起System.Collenctions和System.Collenctions.Generic命名空间中的列表、集合和数组来说,并发集合会有更大的开销。因此,应该只在需要从多个任务中并发访问集合的时候才使用并发几个,在串行代码中使用并发集合是没有意义的,因为它们会增加无谓的开销。为此,在.NET Framework中提供了System.Collections.Concurrent新的命名空间可以访问用于解决线程安全问题,通过这个命名空间能访问以下为并发做好了准备的集合。
2.基本方法
ConcurrentQueue:完全无锁,但面临资源竞争失败时可能会陷入自旋并重试操作。
Enqueue:在队尾插入元素
TryDequeue:尝试删除队头元素,并通过out参数返回
TryPeek:尝试将对头元素通过out参数返回,但不删除该元素。
ConcurrentStack:完全无锁,但面临资源竞争失败时可能会陷入自旋并重试操作。
Push:向栈顶插入元素
TryPop:从栈顶弹出元素,并且通过out 参数返回
TryPeek:返回栈顶元素,但不弹出。
ConcurrentBag:一个无序的集合,程序可以向其中插入元素,或删除元素。在同一个线程中向集合插入,删除元素的效率很高。
Add:向集合中插入元素
TryTake:从集合中取出元素并删除
TryPeek:从集合中取出元素,但不删除该元素。
BlockingCollection:一个支持界限和阻塞的容器
Add :向容器中插入元素
TryTake:从容器中取出元素并删除
TryPeek:从容器中取出元素,但不删除。
CompleteAdding:告诉容器,添加元素完成。此时如果还想继续添加会发生异常。
IsCompleted:告诉消费线程,生产者线程还在继续运行中,任务还未完成。
ConcurrentDictionary:对于读操作是完全无锁的,当很多线程要修改数据时,它会使用细粒度的锁。
AddOrUpdate:如果键不存在,方法会在容器中添加新的键和值,如果存在,则更新现有的键和值。
GetOrAdd:如果键不存在,方法会向容器中添加新的键和值,如果存在则返回现有的值,并不添加新值。
TryAdd:尝试在容器中添加新的键和值。
TryGetValue:尝试根据指定的键获得值。
TryRemove:尝试删除指定的键。
TryUpdate:有条件的更新当前键所对应的值。
GetEnumerator:返回一个能够遍历整个容器的枚举器。
下面看代码,代码中并没有实现线程安全和串行化:
using System; using System.Collections.Generic; using System.Threading.Tasks; namespace ConcurrentDemo { class Program { private static List<Student> students; static void Main(string[] args) { students = new List<Student>(); /*创建任务 t1 t1 执行 数据集合添加操作*/ Task t1 = Task.Factory.StartNew(() => { AddStudent(); }); /*创建任务 t2 t2 执行 数据集合添加操作*/ Task t2 = Task.Factory.StartNew(() => { AddStudent(); }); /*创建任务 t3 t3 执行 数据集合添加操作*/ Task t3 = Task.Factory.StartNew(() => { AddStudent(); }); Task.WaitAll(t1, t2, t3); Console.WriteLine(students.Count); Console.ReadLine(); } static void AddStudent() { Parallel.For(0, 1000, (i) => { Student student = new Student(); student.Id = i; student.Name = "name" + i; students.Add(student); }); } } class Student { public int Id { get; set; } public string Name { get; set; } } }
代码中开启了三个并发操作,每个操作都向集合中添加1000条数据,在没有保障线程安全和串行化的运行下,实际得到的数据并没有3000条,结果如下:
为此我们需要采用Lock关键字,来确保每次只有一个线程来访问 students.Add(student); 这个方法,代码如下:
using System; using System.Collections.Generic; using System.Threading.Tasks; namespace ConcurrentDemo { class Program { private static object obj = new object(); private static List<Student> students; static void Main(string[] args) { students = new List<Student>(); /*创建任务 t1 t1 执行 数据集合添加操作*/ Task t1 = Task.Factory.StartNew(() => { AddStudent(); }); /*创建任务 t2 t2 执行 数据集合添加操作*/ Task t2 = Task.Factory.StartNew(() => { AddStudent(); }); /*创建任务 t3 t3 执行 数据集合添加操作*/ Task t3 = Task.Factory.StartNew(() => { AddStudent(); }); Task.WaitAll(t1, t2, t3); Console.WriteLine(students.Count); Console.ReadLine(); } static void AddStudent() { Parallel.For(0, 1000, (i) => { Student student = new Student(); student.Id = i; student.Name = "name" + i; lock (obj) { students.Add(student); } }); } } class Student { public int Id { get; set; } public string Name { get; set; } } }
但是锁的引入,带来了一定的开销和性能的损耗,并降低了程序的扩展性,在并发编程中显然不适用。
上述例子改成使用ConcurrentBag类型:
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; namespace ConcurrentDemo { class Program { //private static object obj = new object(); private static ConcurrentBag<Student> students; static void Main(string[] args) { students = new ConcurrentBag<Student>(); /*创建任务 t1 t1 执行 数据集合添加操作*/ Task t1 = Task.Factory.StartNew(() => { AddStudent(); }); /*创建任务 t2 t2 执行 数据集合添加操作*/ Task t2 = Task.Factory.StartNew(() => { AddStudent(); }); /*创建任务 t3 t3 执行 数据集合添加操作*/ Task t3 = Task.Factory.StartNew(() => { AddStudent(); }); Task.WaitAll(t1, t2, t3); Console.WriteLine(students.Count); Console.ReadLine(); } static void AddStudent() { Parallel.For(0, 1000, (i) => { Student student = new Student(); student.Id = i; student.Name = "name" + i; students.Add(student); }); } } class Student { public int Id { get; set; } public string Name { get; set; } } }
返回结果也是3000
具体每种集合的API可以查看 https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent?redirectedfrom=MSDN&view=net-6.0
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器