批量修改集合对象属性的实现方案、性能对比及并发集合选型咨询
你好!针对你提出的「快速修改集合中对象属性」的需求,我整理了不同实现方案的代码、性能测试结果,以及相关问题的分析,一起来看看吧:
测试代码实现
我用BenchmarkDotNet做了五种不同实现方式的性能测试,完整代码如下:
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public delegate void PtEventHandler(object source, EventArgs e); public class Pt { public string Nr { get; set; } public int X { get; set; } public int Y { get; set; } public bool Selected { get; set; } public void OnPtChanged(object source, EventArgs e) { Selected = true; } } public class Tests { private Dictionary<string, Pt> dict = new Dictionary<string, Pt>(); public event PtEventHandler PtChanged; [GlobalSetup] public void Setup() { for (int i = 0; i < 10000; i++) { var p = new Pt { Nr = i.ToString(), X = i, Y = i, Selected = false }; PtChanged += p.OnPtChanged; dict.Add(i.ToString(), p); } } [Benchmark] public void Linq() { dict.Values.ToList().ForEach(p => p.Selected = true); } [Benchmark] public void Loop() { for (int i = 0; i < 10000; i++) dict[i.ToString()].Selected = true; } [Benchmark] public void Event() { PtChanged(this, EventArgs.Empty); } [Benchmark] public void ParallelLinq() { dict.Values.AsParallel().ForAll(p => p.Selected = true); } [Benchmark] public void ParallelLoop() { Parallel.For(0, 10000, i => { dict[i.ToString()].Selected = true; }); } } // 测试入口 class Program { static void Main() { var result = BenchmarkRunner.Run<Tests>(); Console.ReadKey(); } }
性能测试结果
以下是10000个对象的测试数据(单位:微秒us,数值越小性能越好):
| Method | Mean | Error | StdDev |
|---|---|---|---|
| Linq | 61.59 us | 1.581 us | 4.662 us |
| Loop | 306.84 us | 6.123 us | 16.448 us |
| Event | 44.25 us | 0.871 us | 1.503 us |
| ParallelLinq | 355.78 us | 7.064 us | 17.723 us |
| ParallelLoop | 119.16 us | 0.977 us | 0.816 us |
方案性能分析
Event事件驱动:性能最优,耗时仅44us左右。原理是初始化时给每个对象绑定事件处理方法,触发事件时所有对象会同时执行属性修改,完全避免了遍历集合的开销。但要注意:这种方式只适合全量修改的场景,如果是修改选中对象,事件驱动就不好精准控制(硬要加判断的话,会抵消大部分性能优势)。
Linq遍历:性能次之,61us左右。用
ToList().ForEach的方式代码简洁,但内部做了一次ToList的内存拷贝,不过比普通循环快很多,适合追求代码简洁的全量修改场景。ParallelLoop并行循环:119us左右,比单线程Linq慢,但比普通单线程Loop快。不过并行带来的线程调度开销在小数据量下会很明显,只有当数据量特别大(比如百万级以上)时,并行的优势才会体现出来。
普通Loop循环:性能最差,300us+。核心瓶颈是每次都要通过
i.ToString()作为Key去字典里查找对象,字符串拼接和字典查找的开销累加起来非常大,这种实现方式不推荐。ParallelLinq并行Linq:性能垫底,355us+。同样是并行调度的额外开销,加上PLinq本身的处理逻辑,小数据量下完全不划算,不建议使用。
关于并发集合的选型问题
你问的「能不能用AsParallel或Parallel配合普通Dictionary,还是必须用ConcurrentDictionary」,分两种场景来看:
- 仅修改对象内部属性(不改动字典结构):像你测试里的场景,只是修改字典中已有对象的
Selected属性(不增删字典的Key/Value),普通Dictionary是可以安全使用的。因为.NET的Dictionary在仅读取Key/Value、不修改集合结构的情况下,并行访问不会有线程安全问题。 - 需要修改字典结构:如果你的并行操作涉及到字典的增删改(比如Add、Remove、Clear),那必须用
ConcurrentDictionary。普通Dictionary没有做并发安全设计,多线程修改集合结构会导致字典损坏、数据丢失甚至抛出异常,而ConcurrentDictionary专门针对并发场景做了优化,能安全处理这类操作。
内容来源于stack exchange

