网站建设员工资摄影网站建设的意义
2025/12/30 0:48:40 网站建设 项目流程
网站建设员工资,摄影网站建设的意义,济南网络电视台,汶上网站制作代码中反复出现的 “集合已修改#xff1b;可能无法执行枚举操作”#xff08;InvalidOperationException: Collection was modified; enumeration operation may not execute.#xff09;异常#xff0c;主要源于在遍历 XValues 或 YValues 的同时#xff08;直接或间接地…代码中反复出现的“集合已修改可能无法执行枚举操作”InvalidOperationException: Collection was modified; enumeration operation may not execute.异常主要源于在遍历XValues或YValues的同时直接或间接地对这些Listdouble进行了添加或清除操作。最典型的问题出现在以下方法中XAutoScale_()和YAutoScale()在调用Max()/Min()时如果外部代码在另一个线程中同时向XValues/YValues添加数据就会触发此异常。Add()方法系列本身是安全的但如果在绘制或自动缩放的过程中被多线程调用也会间接导致问题。优化目标解决集合修改枚举异常核心问题提高线程安全适用于实时数据采集场景修复潜在 bug如日志轴负值、边界处理提升性能和代码可读性优化后的完整Channel类重点修改部分已注释说明usingCommonInterface;usingSystem;usingSystem.Collections.Generic;usingSystem.Diagnostics;usingSystem.Drawing;usingSystem.Linq;namespaceCommonCtrlLib{[Serializable]publicclassChannel{// 使用线程安全的快照机制避免枚举时集合被修改privatereadonlyobject_locknewobject();publicListdoubleXValues{get;privateset;}publicListdoubleYValues{get;privateset;}publicListPointFCoordValues{get;privateset;}publicChannelPropsChannelProps{get;set;}publicdoubleXUnitFactor{get;set;}1;publicdoubleYUnitFactor{get;set;}1;publicboolCleared{get;set;}publicboolXAutoLogAxisLimit{get;set;}true;publicboolYAutoLogAxisLimit{get;set;}true;publicChannel(){XValuesnewListdouble();YValuesnewListdouble();CoordValuesnewListPointF();ChannelPropsnewChannelProps(this){ChartModeDrawingMode.XYScatter,LineColorColor.Red};}publicChannel(Colorcolor):this(){ChannelProps.LineColorcolor;}#region数据添加方法线程安全publicvoidAdd(doublex,doubley){lock(_lock){XValues.Add(x);YValues.Add(y);}}publicvoidAdd(Listlongtimes,double[]values){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Length);lock(_lock){for(inti0;icount;i){XValues.Add(times[i]);YValues.Add(values[i]);}}}publicvoidAdd(Listdoubletimes,double[]values){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Length);lock(_lock){for(inti0;icount;i){XValues.Add(times[i]);YValues.Add(values[i]);}}}publicvoidAdd(ListdoublexList,ListdoubleyList){if(xListnull||yListnull)return;intcountMath.Min(xList.Count,yList.Count);lock(_lock){for(inti0;icount;i){XValues.Add(xList[i]);YValues.Add(yList[i]);}}}publicvoidAdd(Listlongtimes,Listdoublevalues){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Count);lock(_lock){for(inti0;icount;i){XValues.Add(times[i]);YValues.Add(values[i]);}}}#endregion#region轴限设置publicvoidSetYAxisLimit(doublemax,doublemin,doubleoffset){doubleyMaxmax*YUnitFactor;doubleyMinmin*YUnitFactor;doubleoffoffset*YUnitFactor;if(ChannelProps.YAxisDisplayModeAxisDisplayMode.Log){if(yMax0||yMin0)return;// 对数轴不允许非正值yMaxMath.Log10(yMax);yMinMath.Log10(yMin);if(YAutoLogAxisLimit)RecomputeAxisLimit(refyMax,refyMin);else{yMinMath.Floor(yMin);yMaxMath.Ceiling(yMax);}offoff0?0:Math.Log10(off);}elseif(ChannelProps.YAxisDisplayModeAxisDisplayMode.Integer){yMaxMath.Floor(yMax);yMinMath.Ceiling(yMin);}AdjustZeroRange(refyMax,refyMin);if(double.IsNaN(yMax)||double.IsNaN(yMin)||double.IsNaN(off))return;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalMinyMin;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffsetoff;}publicvoidSetXAxisLimit(doublemax,doublemin,doubleoffset){doublexMaxmax*XUnitFactor;doublexMinmin*XUnitFactor;doubleoffoffset*XUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.Log){if(xMax0||xMin0)return;xMaxMath.Log10(xMax);xMinMath.Log10(xMin);if(XAutoLogAxisLimit)RecomputeAxisLimit(refxMax,refxMin);offoff0?0:Math.Log10(off);}elseif(ChannelProps.XAxisDisplayModeAxisDisplayMode.Integer){xMaxMath.Floor(xMax);xMinMath.Ceiling(xMin);}AdjustZeroRange(refxMax,refxMin);if(double.IsNaN(xMax)||double.IsNaN(xMin)||double.IsNaN(off))return;ChannelProps.HorizonMaxxMax;ChannelProps.HorizonMinxMin;ChannelProps.HorizonStep0.1*(xMax-xMin);ChannelProps.XOffsetoff;}privatevoidAdjustZeroRange(refdoublemax,refdoublemin){if(Math.Abs(max-min)1E-8){if(max!0){max2*max;min0;}else{max10;min0;}}}privatevoidRecomputeAxisLimit(refdoublemax,refdoublemin){constintspaceCount10;minMath.Floor(min);maxMath.Ceiling(max);doublestepMath.Ceiling((max-min)/spaceCount);maxminstep*spaceCount;}#endregion#region自动缩放关键使用快照避免枚举异常publicvoidAutoScale(){double[]xSnapshot;double[]ySnapshot;lock(_lock){if(XValues.Count0||YValues.Count0)return;xSnapshotXValues.ToArray();ySnapshotYValues.ToArray();}doublexMinxSnapshot.Min()*XUnitFactor;doublexMaxxSnapshot.Max()*XUnitFactor;doubleyMinySnapshot.Min()*YUnitFactor;doubleyMaxySnapshot.Max()*YUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.Log){if(xMin0||xMax0)return;xMinMath.Log10(xMin);xMaxMath.Log10(xMax);}if(ChannelProps.YAxisDisplayModeAxisDisplayMode.Log){if(yMin0||yMax0)return;yMinMath.Log10(yMin);yMaxMath.Log10(yMax);}if(double.IsNaN(xMin)||double.IsNaN(xMax)||double.IsNaN(yMin)||double.IsNaN(yMax))return;ChannelProps.HorizonMinxMin;ChannelProps.HorizonMaxxMax;ChannelProps.HorizonStep0.1*(xMax-xMin);ChannelProps.XOffset0;ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffset0;}/// summary/// 推荐使用此方法线程安全处理对数轴负值/// /summarypublicvoidXAutoScale(){double[]snapshot;lock(_lock){if(XValues.Count0)return;snapshotXValues.ToArray();}doublerawMinsnapshot.Min()*XUnitFactor;doublerawMaxsnapshot.Max()*XUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.Log){varpositivesnapshot.Where(vv*XUnitFactor0).ToArray();if(positive.Length0)return;doubleminPospositive.Min()*XUnitFactor;doublemaxPospositive.Max()*XUnitFactor;rawMinMath.Log10(minPos);rawMaxMath.Log10(maxPos);}else{rawMinrawMin;rawMaxrawMax;}if(double.IsNaN(rawMin)||double.IsNaN(rawMax))return;ChannelProps.HorizonMinrawMin;ChannelProps.HorizonMaxrawMax;ChannelProps.HorizonStep0.1*(rawMax-rawMin);ChannelProps.XOffset0;}publicvoidYAutoScale(){double[]snapshot;lock(_lock){if(YValues.Count0)return;snapshotYValues.ToArray();}doubleyMinsnapshot.Min()*YUnitFactor;doubleyMaxsnapshot.Max()*YUnitFactor;// 你原代码这里有 bugyMin 0 ? 0 : ... 会导致负值被强制为0破坏对称性// 建议移除除非业务明确要求 Y 轴从0开始// yMin yMin 0 ? 0 : yMin;if(ChannelProps.YAxisDisplayModeAxisDisplayMode.Log){if(yMin0||yMax0)return;yMinMath.Log10(yMin);yMaxMath.Log10(yMax);}if(double.IsNaN(yMin)||double.IsNaN(yMax))return;ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffset0;}#endregionpublicvoidClear(){lock(_lock){XValues.Clear();YValues.Clear();CoordValues.Clear();}Clearedtrue;}publicvoidChangeAxisXUnit(doublexUnitFactor){XUnitFactorxUnitFactor;}publicvoidChangeAxisYUnit(doubleyUnitFactor){YUnitFactoryUnitFactor;}publicvoidResample(intsampleCount){lock(_lock){if(XValues.Count2)return;ResamplerMgr.Resample2(sampleCount,XValues,YValues,outListdoublex,outListdoubley);XValuesx;YValuesy;}}}}优化总结问题解决方案集合被修改导致枚举异常所有读操作如 Max/Min前使用ToArray()快照 lock多线程数据添加冲突所有Add、Clear、Resample加锁对数轴处理负值/零增加合法性检查返回或跳过YAutoScale 中强制 yMin0 的潜在 bug注释掉除非明确需求代码重复提取公共方法如 AdjustZeroRange性能快照方式开销很小远好于异常崩溃这样修改后你的通道类在实时采集 UI 刷新场景下将非常稳定不再出现“集合已修改”异常。如需更高性能超高频率数据可考虑使用ConcurrentQueue或环形缓冲区但当前lock List已足够大多数工程应用。是的除了使用lock互斥锁来实现线程安全还有几种更好的替代方案尤其适合实时数据采集 图表显示这种高频添加、低频读取的场景。下面我按推荐优先级列出几种常见且实用的替代方法并对比优缺点帮助你选择最合适的。方案是否需要锁性能复杂度适用场景推荐度1. 生产者-消费者模式双缓冲 / Swap Buffer无正确实现后最高中等实时波形显示最常见★★★★★2. ConcurrentQueue / ConcurrentBag无高低只需追加、不关心顺序★★★★3. Immutable List 原子替换Interlocked无高中等需要完整快照读取★★★★4. ReaderWriterLockSlim有读写锁中高中读多写少★★★5. 普通 lock你当前方案有中低简单场景★★推荐方案 1双缓冲Swap Buffer——最适合波形图这是工业级实时示波器/数据记录仪最常用的方式几乎零锁竞争。核心思路准备两个 List一个“后台缓冲”正在写入新数据一个“前台缓冲”UI 用于绘制和 AutoScale。数据采集线程只往后台缓冲 Add。当需要刷新图表时原子交换两个缓冲引用不需锁或只需极短锁。UI 线程只读取前台缓冲。代码实现示例修改你的 Channel 类publicclassChannel{// 挥发性确保可见性privatevolatileListdouble_frontXnewListdouble();privatevolatileListdouble_frontYnewListdouble();privateListdouble_backXnewListdouble();privateListdouble_backYnewListdouble();privatereadonlyobject_swapLocknewobject();// 只在交换时短暂加锁// 数据添加采集线程调用高频无锁publicvoidAdd(doublex,doubley){_backX.Add(x);_backY.Add(y);}// 其他 Add 重载类似全部改成操作 _backX/_backY// UI 线程调用交换缓冲极短时间加锁publicvoidSwapBuffers(){lock(_swapLock)// 锁住时间极短通常 1ms{vartempX_frontX;vartempY_frontY;_frontX_backX;_frontY_backY;_backXtempX;_backYtempY;_backX.Clear();_backY.Clear();}}// 自动缩放等读取操作只读前台缓冲完全无锁publicvoidAutoScale(){varxValues_frontX;// 引用复制原子操作varyValues_frontY;if(xValues.Count0||yValues.Count0)return;doublexMinxValues.Min()*XUnitFactor;doublexMaxxValues.Max()*XUnitFactor;// ... 其余逻辑不变}publicvoidClear(){lock(_swapLock){_backX.Clear();_backY.Clear();_frontX.Clear();_frontY.Clear();}Clearedtrue;}}优点数据添加完全无锁采集线程不会阻塞。UI 刷新只在 Swap 时极短暂阻塞通常毫秒级。完美解决“集合已修改”异常。使用方式数据采集线程不停调用channel.Add(x, y)UI 定时器例如每 100mschannel.SwapBuffers(); channel.AutoScale(); chart.Invalidate();推荐方案 2使用 ConcurrentQueue如果顺序不严格要求如果你不关心点的绝对顺序波形图通常不关心只要大致时间顺序即可usingSystem.Collections.Concurrent;publicConcurrentQueuedoubleXValues{get;}newConcurrentQueuedouble();publicConcurrentQueuedoubleYValues{get;}newConcurrentQueuedouble();publicvoidAdd(doublex,doubley){XValues.Enqueue(x);YValues.Enqueue(y);}// 读取时转数组ToArray 是线程安全的快照publicvoidAutoScale(){if(XValues.IsEmpty)return;varxArrayXValues.ToArray();varyArrayYValues.ToArray();// 计算 Min/Max}优点完全无锁性能极高缺点无法 Clear只能不断 Enqueue内存会持续增长 → 需定期清理或结合双缓冲推荐方案 3环形缓冲区固定大小高性能如果数据点很多但只需显示最近 N 个点如示波器滚动模式可以使用第三方库如 CircularBuffer 或自己实现。总结建议你的场景推荐方案实时波形显示需要平滑刷新、无卡顿双缓冲Swap Buffer← 强烈推荐数据量不大简单可靠优先保留 lock 也行数据极高频10k点/秒对性能极致要求双缓冲 环形缓冲不需要保持顺序只追加ConcurrentQueue最推荐你现在改成双缓冲模式它几乎是为你这种“一边采集一边画图”场景量身定做的工业界标准做法彻底告别“集合已修改”异常。需要我帮你完整重写成双缓冲版本的 Channel 类吗生产者-消费者模式Producer-Consumer Pattern是一种经典的并发设计模式用于解耦“生产数据”和“消费数据”的线程。生产者Producer负责生成数据如实时采集传感器数据、接收网络包。消费者Consumer负责处理数据如计算统计、绘制波形、保存日志。缓冲区Buffer/Queue中间存储容器生产者放入数据消费者取出数据。这种模式特别适合你的实时波形显示场景采集线程生产者高频添加数据点UI线程消费者定期读取数据进行AutoScale和绘图避免直接在共享List上操作导致“集合已修改”异常。C# 中实现生产者-消费者模式的几种常见方式方式核心类是否无锁/低锁适用场景优缺点BlockingCollection推荐入门System.Collections.Concurrent.BlockingCollectionT内部锁线程安全简单生产-消费需阻塞等待简单易用支持 bounded限容防止内存爆炸Take() 会阻塞直到有数据Channel现代推荐.NET Core/5System.Threading.Channels.ChannelT无锁高性能高性能、异步场景支持 unbounded/bounded、单/多生产者消费者异步 Await 更流畅双缓冲交换Swap Buffer最适合实时波形两个Listdoublevolatile或短锁交换添加完全无锁交换极短锁采集高频、UI 定期刷新波形生产无阻塞消费读取快照无异常工业级示波器标准做法ConcurrentQueueSystem.Collections.Concurrent.ConcurrentQueueT无锁只追加、不需顺序严格高性能但无 bounded内存易增长需定期清理下面针对你的Channel类推荐两种实现双缓冲交换最匹配你的需求和BlockingCollection简单备选。方案1双缓冲交换强烈推荐用于实时波形采集线程无锁添加UI线程每50-100ms交换一次缓冲读取前台缓冲计算Min/Max/AutoScale。usingSystem;usingSystem.Collections.Generic;usingSystem.Drawing;namespaceCommonCtrlLib{[Serializable]publicclassChannel{// 前台UI读取用的快照只读privatevolatileListdouble_frontXnewListdouble();privatevolatileListdouble_frontYnewListdouble();// 后台采集线程写入privateListdouble_backXnewListdouble();privateListdouble_backYnewListdouble();privatereadonlyobject_swapLocknewobject();// 交换时极短锁publicChannelPropsChannelProps{get;set;}publicdoubleXUnitFactor{get;set;}1;publicdoubleYUnitFactor{get;set;}1;publicboolCleared{get;set;}// 其他属性略...publicChannel(){ChannelPropsnewChannelProps(this){ChartModeDrawingMode.XYScatter,LineColorColor.Red};}// 采集线程调用高频添加完全无锁publicvoidAdd(doublex,doubley){_backX.Add(x);_backY.Add(y);}// 其他Add重载类似全部操作 _backX/_backY// UI线程调用定时器每50~100ms一次交换缓冲publicvoidSwapAndPrepareForDraw(){ListdoubletempX;ListdoubletempY;lock(_swapLock)// 锁时间极短{tempX_frontX;tempY_frontY;_frontX_backX;_frontY_backY;_backXtempX;_backYtempY;_backX.Clear();_backY.Clear();}}// AutoScale / XAutoScale / YAutoScale 等读取操作直接读 _front无锁publicvoidAutoScale(){varxValues_frontX;// 引用原子复制varyValues_frontY;if(xValues.Count0||yValues.Count0)return;// 正常计算 Min/Max ...代码同之前但用 xValues/yValuesdoublexMinxValues.Min()*XUnitFactor;doublexMaxxValues.Max()*XUnitFactor;// ... 对数轴等处理// 设置 ChannelProps ...}publicvoidClear(){lock(_swapLock){_backX.Clear();_backY.Clear();_frontX.Clear();_frontY.Clear();}Clearedtrue;}// Resample 等也建议在Swap后对 _front 操作或加锁}}使用方式采集线程channel.Add(x, y);UI定时器Timer或DispatcherTimerchannel.SwapAndPrepareForDraw();channel.AutoScale();// 或 XAutoScale / YAutoScalechartControl.Invalidate();// 重绘优点采集零阻塞UI读取稳定快照彻底杜绝枚举异常。方案2使用 BlockingCollection简单但可能有轻微阻塞usingSystem.Collections.Concurrent;// 在Channel中privatereadonlyBlockingCollectionPointD_queuenewBlockingCollectionPointD(boundedCapacity:100000);// 可限容publicvoidAdd(doublex,doubley){_queue.Add(newPointD{Xx,Yy});}publicvoidConsumeAllForScale(){ListdoublexListnewListdouble();ListdoubleyListnewListdouble();while(_queue.TryTake(outPointDpoint)){xList.Add(point.X);yList.Add(point.Y);}// 用 xList/yList 计算 AutoScale}UI定期调用ConsumeAllForScale()并绘图。生产者-消费者模式的核心就是通过缓冲区解耦速度差异。如果你采用双缓冲方案你的实时波形将非常流畅稳定。如果需要完整代码或其他方案如Channel异步版随时告诉我下面是基于双缓冲Swap Buffer生产者-消费者模式的完整Channel类实现版本。这个版本完全解决了“集合已修改可能无法执行枚举操作”的异常同时实现了数据采集线程高频无锁添加生产者UI 线程安全读取快照进行 AutoScale 和绘图消费者极少的锁竞争仅在 Swap 时短暂加锁支持所有原有功能Add 多重载、AutoScale、X/Y AutoScale、对数轴、单位换算、Clear、Resample 等usingCommonInterface;usingSystem;usingSystem.Collections.Generic;usingSystem.Diagnostics;usingSystem.Drawing;usingSystem.Linq;namespaceCommonCtrlLib{[Serializable]publicclassChannel{// 前台缓冲UI 线程用于读取、计算 AutoScale、绘图只读快照privatevolatileListdouble_frontXnewListdouble();privatevolatileListdouble_frontYnewListdouble();// 后台缓冲采集线程用于写入新数据privateListdouble_backXnewListdouble();privateListdouble_backYnewListdouble();// 交换时使用的极短锁privatereadonlyobject_swapLocknewobject();publicListPointFCoordValues{get;set;}newListPointF();// 如需可同步处理这里暂不双缓冲publicChannelPropsChannelProps{get;set;}publicdoubleXUnitFactor{get;set;}1.0;publicdoubleYUnitFactor{get;set;}1.0;publicboolCleared{get;set;}publicboolXAutoLogAxisLimit{get;set;}true;publicboolYAutoLogAxisLimit{get;set;}true;publicChannel(){ChannelPropsnewChannelProps(this){ChartModeDrawingMode.XYScatter,LineColorColor.Red};}publicChannel(Colorcolor):this(){ChannelProps.LineColorcolor;}#region数据添加生产者线程调用高频无锁publicvoidAdd(doublex,doubley){_backX.Add(x);_backY.Add(y);}publicvoidAdd(Listlongtimes,double[]values){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Length);for(inti0;icount;i){_backX.Add(times[i]);_backY.Add(values[i]);}}publicvoidAdd(Listdoubletimes,double[]values){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Length);for(inti0;icount;i){_backX.Add(times[i]);_backY.Add(values[i]);}}publicvoidAdd(ListdoublexList,ListdoubleyList){if(xListnull||yListnull)return;intcountMath.Min(xList.Count,yList.Count);for(inti0;icount;i){_backX.Add(xList[i]);_backY.Add(yList[i]);}if(xList.Count!yList.Count){Debug.WriteLine(Add: xList.Count ! yList.Count);}}publicvoidAdd(Listlongtimes,Listdoublevalues){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Count);for(inti0;icount;i){_backX.Add(times[i]);_backY.Add(values[i]);}}#endregion#region缓冲区交换UI 线程调用建议每 50~200ms 调用一次/// summary/// 交换前后缓冲区使新数据成为可绘制快照并清空后台缓冲准备接收新数据/// /summarypublicvoidSwapBuffers(){lock(_swapLock){// 交换引用vartempX_frontX;vartempY_frontY;_frontX_backX;_frontY_backY;_backXtempX;_backYtempY;// 清空后台准备下一轮采集_backX.Clear();_backY.Clear();}}#endregion#region自动缩放消费者线程调用使用前台快照完全无锁publicvoidAutoScale(){varxValues_frontX;varyValues_frontY;if(xValues.Count0||yValues.Count0)return;doublexMinxValues.Min()*XUnitFactor;doublexMaxxValues.Max()*XUnitFactor;doubleyMinyValues.Min()*YUnitFactor;doubleyMaxyValues.Max()*YUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.Log){if(xMin0||xMax0)return;xMinMath.Log10(xMin);xMaxMath.Log10(xMax);}if(ChannelProps.YAxisDisplayModeAxisDisplayMode.Log){if(yMin0||yMax0)return;yMinMath.Log10(yMin);yMaxMath.Log10(yMax);}if(double.IsNaN(xMin)||double.IsNaN(xMax)||double.IsNaN(yMin)||double.IsNaN(yMax))return;ChannelProps.HorizonMinxMin;ChannelProps.HorizonMaxxMax;ChannelProps.HorizonStep0.1*(xMax-xMin);ChannelProps.XOffset0;ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffset0;}publicvoidXAutoScale(){varxValues_frontX;if(xValues.Count0)return;doublerawMinxValues.Min()*XUnitFactor;doublerawMaxxValues.Max()*XUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.Log){varpositivexValues.Where(vv*XUnitFactor0);if(!positive.Any())return;doubleminPospositive.Min()*XUnitFactor;doublemaxPospositive.Max()*XUnitFactor;rawMinMath.Log10(minPos);rawMaxMath.Log10(maxPos);}if(double.IsNaN(rawMin)||double.IsNaN(rawMax))return;ChannelProps.HorizonMinrawMin;ChannelProps.HorizonMaxrawMax;ChannelProps.HorizonStep0.1*(rawMax-rawMin);ChannelProps.XOffset0;}publicvoidYAutoScale(){varyValues_frontY;if(yValues.Count0)return;doubleyMinyValues.Min()*YUnitFactor;doubleyMaxyValues.Max()*YUnitFactor;if(ChannelProps.YAxisDisplayModeAxisDisplayMode.Log){if(yMin0||yMax0)return;yMinMath.Log10(yMin);yMaxMath.Log10(yMax);}if(double.IsNaN(yMin)||double.IsNaN(yMax))return;ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffset0;}#endregion#region轴限设置可基于当前前台数据或外部传入publicvoidSetYAxisLimit(doublemax,doublemin,doubleoffset){// 与原逻辑相同直接使用传入值通常由外部计算// ...保持你原来的 SetYAxisLimit 实现// 为了篇幅这里略去可直接复制原方法}publicvoidSetXAxisLimit(doublemax,doublemin,doubleoffset){// 同上}privatevoidRecomputeAxisLimit(refdoublemax,refdoublemin){constintspaceCount10;minMath.Floor(min);maxMath.Ceiling(max);doublestepMath.Ceiling((max-min)/spaceCount);maxminstep*spaceCount;}#endregion#region其他操作publicvoidClear(){lock(_swapLock){_backX.Clear();_backY.Clear();_frontX.Clear();_frontY.Clear();CoordValues.Clear();}Clearedtrue;}publicvoidChangeAxisXUnit(doublexUnitFactor){XUnitFactorxUnitFactor;}publicvoidChangeAxisYUnit(doubleyUnitFactor){YUnitFactoryUnitFactor;}publicvoidResample(intsampleCount){varcurrentX_frontX;// 重采样当前显示数据varcurrentY_frontY;if(currentX.Count2)return;lock(_swapLock)// 防止 Swap 时冲突{ResamplerMgr.Resample2(sampleCount,currentX,currentY,outListdoublenewX,outListdoublenewY);// 替换前台数据下一帧生效_frontXnewX;_frontYnewY;// 后台继续接收新数据}}/// summary/// 获取当前用于绘图的数据快照线程安全/// /summarypublic(ListdoubleX,ListdoubleY)GetCurrentDataSnapshot(){return(_frontX,_frontY);}#endregion}}使用示例UI 线程// 定时器 Tick 事件例如每 100msprivatevoidRefreshTimer_Tick(objectsender,EventArgse){foreach(varchannelinchannels){channel.SwapBuffers();// 交换缓冲获取最新数据channel.AutoScale();// 或 XAutoScale() / YAutoScale()}chartControl.Invalidate();// 触发重绘}优点总结采集线程Add()完全无锁零阻塞UI 线程读取稳定快照永不抛出“集合已修改”异常锁仅在 Swap 时使用且时间极短通常 1ms完美支持实时波形显示、自动缩放、对数轴等所有功能下面是为你的Channel类提供的**环形缓冲区Circular Buffer**完整实现版本。环形缓冲区特别适合实时波形显示场景因为固定内存占用无论采集多久内存都不会无限增长只保留最近 N 个点高性能添加数据 O(1)无锁或极低锁竞争自然滚动效果旧数据自动被覆盖新数据从左侧进入形成示波器式的滚动波形完整环形缓冲区版Channel类usingSystem;usingSystem.Collections.Generic;usingSystem.Drawing;usingSystem.Linq;namespaceCommonCtrlLib{[Serializable]publicclassChannel{// 环形缓冲区参数privatereadonlyint_capacity;// 缓冲区最大点数例如 100000privatedouble[]_xBuffer;privatedouble[]_yBuffer;privateint_head0;// 下一个写入位置头部privateint_count0;// 当前有效数据点数privatebool_wrappedfalse;// 是否已经环绕一圈privatereadonlyobject_writeLocknewobject();// 写入锁可选视频率而定publicChannelPropsChannelProps{get;set;}publicdoubleXUnitFactor{get;set;}1.0;publicdoubleYUnitFactor{get;set;}1.0;publicboolCleared{get;set;}publicboolXAutoLogAxisLimit{get;set;}true;publicboolYAutoLogAxisLimit{get;set;}true;/// summary/// 构造函数/// /summary/// param namecapacity缓冲区容量最大存储点数推荐 10000 ~ 500000/parampublicChannel(intcapacity100000){if(capacity0)thrownewArgumentException(Capacity must be 0);_capacitycapacity;_xBuffernewdouble[_capacity];_yBuffernewdouble[_capacity];ChannelPropsnewChannelProps(this){ChartModeDrawingMode.XYScatter,LineColorColor.Red};}publicChannel(intcapacity,Colorcolor):this(capacity){ChannelProps.LineColorcolor;}#region数据添加生产者高频写入publicvoidAdd(doublex,doubley){lock(_writeLock)// 如果采集频率极高且多线程可去掉锁用 Interlocked更复杂{_xBuffer[_head]x;_yBuffer[_head]y;_head(_head1)%_capacity;if(_count_capacity)_count;else_wrappedtrue;// 已满开始覆盖旧数据}}// 批量添加性能更好publicvoidAdd(ListdoublexList,ListdoubleyList){if(xListnull||yListnull)return;intcountMath.Min(xList.Count,yList.Count);lock(_writeLock){for(inti0;icount;i){_xBuffer[_head]xList[i];_yBuffer[_head]yList[i];_head(_head1)%_capacity;if(_count_capacity)_count;else_wrappedtrue;}}}// 支持 long 时间戳publicvoidAdd(Listlongtimes,Listdoublevalues){if(timesnull||valuesnull)return;intcountMath.Min(times.Count,values.Count);lock(_writeLock){for(inti0;icount;i){_xBuffer[_head]times[i];_yBuffer[_head]values[i];_head(_head1)%_capacity;if(_count_capacity)_count;else_wrappedtrue;}}}// 其他 Add 重载类似...#endregion#region获取当前有效数据快照UI 线程使用无锁读取快照/// summary/// 获取当前所有有效数据从旧到新顺序用于绘图和 AutoScale/// /summarypublic(double[]X,double[]Y)GetCurrentData(){double[]xSnapshotnewdouble[_count];double[]ySnapshotnewdouble[_count];lock(_writeLock)// 短暂锁确保读取一致性{if(!_wrapped){// 未环绕直接从 0 开始复制Array.Copy(_xBuffer,0,xSnapshot,0,_count);Array.Copy(_yBuffer,0,ySnapshot,0,_count);}else{// 已环绕先复制尾部再复制头部inttailCount_capacity-(_head);// head 之前的数据是旧的不对// 正确顺序最旧的数据在 head 位置开始向后到末尾 0 到 head-1intfirstPart_capacity-_head;// 从 head 到末尾旧数据intsecondPart_count-firstPart;// 从 0 开始的新数据// 最旧 - 最旧firstPartArray.Copy(_xBuffer,_head,xSnapshot,0,firstPart);Array.Copy(_yBuffer,_head,ySnapshot,0,firstPart);// 接着是最新的if(secondPart0){Array.Copy(_xBuffer,0,xSnapshot,firstPart,secondPart);Array.Copy(_yBuffer,0,ySnapshot,firstPart,secondPart);}}}return(xSnapshot,ySnapshot);}/// summary/// 获取当前点数/// /summarypublicintCount_count;/// summary/// 是否已满开始覆盖旧数据/// /summarypublicboolIsFull_count_capacity;#endregion#region自动缩放使用快照publicvoidAutoScale(){var(xData,yData)GetCurrentData();if(xData.Length0||yData.Length0)return;doublexMinxData.Min()*XUnitFactor;doublexMaxxData.Max()*XUnitFactor;doubleyMinyData.Min()*YUnitFactor;doubleyMaxyData.Max()*YUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.LogxMin0xMax0){xMinMath.Log10(xMin);xMaxMath.Log10(xMax);}if(ChannelProps.YAxisDisplayModeAxisDisplayMode.LogyMin0yMax0){yMinMath.Log10(yMin);yMaxMath.Log10(yMax);}ChannelProps.HorizonMinxMin;ChannelProps.HorizonMaxxMax;ChannelProps.HorizonStep0.1*(xMax-xMin);ChannelProps.XOffset0;ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffset0;}publicvoidXAutoScale(){var(xData,_)GetCurrentData();if(xData.Length0)return;doublerawMinxData.Min()*XUnitFactor;doublerawMaxxData.Max()*XUnitFactor;if(ChannelProps.XAxisDisplayModeAxisDisplayMode.Log){varpositivexData.Where(vv*XUnitFactor0);if(!positive.Any())return;rawMinMath.Log10(positive.Min()*XUnitFactor);rawMaxMath.Log10(positive.Max()*XUnitFactor);}ChannelProps.HorizonMinrawMin;ChannelProps.HorizonMaxrawMax;ChannelProps.HorizonStep0.1*(rawMax-rawMin);ChannelProps.XOffset0;}publicvoidYAutoScale(){var(_,yData)GetCurrentData();if(yData.Length0)return;doubleyMinyData.Min()*YUnitFactor;doubleyMaxyData.Max()*YUnitFactor;if(ChannelProps.YAxisDisplayModeAxisDisplayMode.LogyMin0){yMinMath.Log10(yMin);yMaxMath.Log10(yMax);}ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;ChannelProps.VerticalStep0.1*(yMax-yMin);ChannelProps.YOffset0;}#endregion#region其他操作publicvoidClear(){lock(_writeLock){_head0;_count0;_wrappedfalse;Array.Clear(_xBuffer,0,_capacity);Array.Clear(_yBuffer,0,_capacity);}Clearedtrue;}publicvoidChangeAxisXUnit(doublefactor)XUnitFactorfactor;publicvoidChangeAxisYUnit(doublefactor)YUnitFactorfactor;// 重采样当前显示数据publicvoidResample(intsampleCount){var(xData,yData)GetCurrentData();if(xData.Length2)return;ResamplerMgr.Resample2(sampleCount,xData.ToList(),yData.ToList(),outListdoublenewX,outListdoublenewY);Clear();// 清空旧数据Add(newX,newY);// 重新写入会自动环形处理}#endregion}}使用方式与之前相同// 创建通道保留最近 200,000 个点varchannelnewChannel(200000,Color.Blue);// 采集线程channel.Add(timestamp,value);// UI 刷新定时器privatevoidTimer_Tick(objectsender,EventArgse){channel.AutoScale();// 或 XAutoScale() / YAutoScale()chartControl.Invalidate();}// 绘图时var(xArray,yArray)channel.GetCurrentData();// 使用 xArray, yArray 绘图优点总结内存固定不会因长时间运行而泄漏添加速度极快O(1)天然支持滚动波形旧数据自动丢弃线程安全写入加锁读取快照完美解决“集合已修改”异常推荐容量10万 ~ 50万点根据刷新率和显示宽度调整足够流畅显示数分钟到数小时数据。如果你希望完全无锁超高频采集可以进一步用Interlocked替换_head但当前版本已足够 99% 的工程场景。需要我提供无锁版本或结合双缓冲环形的混合方案吗Channel 的实现与使用详解ChannelT是 .NET从 .NET Core 3.0 / .NET 5 开始提供的现代高性能生产者-消费者模式工具位于System.Threading.Channels命名空间中。它专门设计用于在多线程/异步环境中安全、高效地传递数据完美替代传统的BlockingCollectionT、ConcurrentQueueT等。它特别适合你的实时波形采集场景采集线程生产者持续写入数据点UI线程消费者定期读取并绘制波形。1. Channel 的核心概念通道Channel一个数据管道有写入端Writer和读取端Reader。生产者调用writer.WriteAsync(item)或writer.TryWrite(item)把数据放入通道。消费者通过reader.ReadAsync()或reader.TryRead(out item)从通道取出数据。容量模式无界通道Unbounded可以无限写入但内存会持续增长慎用。有界通道Bounded设置最大容量满时写入会阻塞或失败防止内存爆炸推荐用于实时采集。2. 基本创建方式usingSystem.Threading.Channels;// 1. 无界通道不推荐长时间运行的实时采集varunboundedChannelChannel.CreateUnboundedPointD();// 2. 有界通道强烈推荐限制最大缓冲点数例如 100万点varoptionsnewBoundedChannelOptions(1000000)// 容量{FullModeBoundedChannelFullMode.Wait,// 满时写入阻塞等待消费者消费// FullMode BoundedChannelFullMode.DropWrite, // 满时丢弃新数据适合丢帧可容忍场景SingleWritertrue,// 如果只有一个采集线程设为 true 可提升性能SingleReadertrue// 如果只有一个 UI 消费线程也设为 true};varboundedChannelChannel.CreateBoundedPointD(options);// 获取读写器varreaderchannel.Reader;varwriterchannel.Writer;3. 在你的 Channel 类中使用 Channel 的完整示例假设我们定义一个数据点结构publicstructPointD{publicdoubleX;publicdoubleY;}修改你的Channel类为使用ChannelPointDusingSystem.Threading.Channels;publicclassChannel{privatereadonlyChannelPointD_dataChannel;publicChannelPropsChannelProps{get;set;}publicdoubleXUnitFactor{get;set;}1.0;publicdoubleYUnitFactor{get;set;}1.0;// 推荐容量 10万 ~ 100万点publicChannel(intcapacity200000){varoptionsnewBoundedChannelOptions(capacity){FullModeBoundedChannelFullMode.Wait,// 满时阻塞写入安全SingleWritertrue,SingleReaderfalse// UI 可能多个地方消费};_dataChannelChannel.CreateBoundedPointD(options);ChannelPropsnewChannelProps(this){ChartModeDrawingMode.XYScatter,LineColorColor.Red};}// 生产者采集线程调用 publicasyncTaskAddAsync(doublex,doubley){await_dataChannel.Writer.WriteAsync(newPointD{Xx,Yy});}// 同步版本如果采集线程不能 awaitpublicvoidAdd(doublex,doubley){// TryWrite 满时返回 false你可以选择丢弃或处理if(!_dataChannel.Writer.TryWrite(newPointD{Xx,Yy})){// 可选记录丢帧、降采样等// Console.WriteLine(缓冲区满丢弃数据点);}}// 批量添加性能更好publicvoidAddBatch(IEnumerablePointDpoints){foreach(varpinpoints){_dataChannel.Writer.TryWrite(p);}}// 消费者UI 线程定期消费 /// summary/// 消费所有当前可用数据用于 AutoScale 和绘图/// /summarypublicListPointDConsumeAllAvailable(){varlistnewListPointD();while(_dataChannel.Reader.TryRead(outvarpoint)){list.Add(point);}returnlist;}/// summary/// 异步消费更适合 WinForms/WPF 使用 Dispatcher/// /summarypublicasyncTaskListPointDConsumeAllAsync(){varlistnewListPointD();awaitforeach(varpointin_dataChannel.Reader.ReadAllAsync()){list.Add(point);}returnlist;}// 自动缩放示例 privateListPointD_currentDatanewListPointD();// 当前用于绘图的数据publicvoidUpdateAndAutoScale(){// 取出所有新数据varnewPointsConsumeAllAvailable();if(newPoints.Count0){_currentData.AddRange(newPoints);// 可选限制显示点数类似环形缓冲if(_currentData.Count500000){_currentData.RemoveRange(0,_currentData.Count-500000);}// 执行自动缩放if(_currentData.Count0){varxValues_currentData.Select(pp.X*XUnitFactor);varyValues_currentData.Select(pp.Y*YUnitFactor);doublexMinxValues.Min();doublexMaxxValues.Max();doubleyMinyValues.Min();doubleyMaxyValues.Max();// 对数轴处理...if(ChannelProps.XAxisDisplayModeAxisDisplayMode.LogxMin0){xMinMath.Log10(xMin);xMaxMath.Log10(xMax);}// ... Y轴类似ChannelProps.HorizonMinxMin;ChannelProps.HorizonMaxxMax;ChannelProps.VerticalMinyMin;ChannelProps.VerticalMaxyMax;// ... 设置 Step 等}}}// 清空publicvoidClear(){_currentData.Clear();while(_dataChannel.Reader.TryRead(out_)){}// 清空通道}// 获取当前绘图数据publicIReadOnlyListPointDGetCurrentData()_currentData;}4. 使用方式总结// 创建varchannelnewChannel(300000);// 采集线程高频while(采集ing){channel.Add(timestamp,value);// TryWrite快速不阻塞}// UI 定时器每 50~100msprivatevoidTimer_Tick(objectsender,EventArgse){channel.UpdateAndAutoScale();chartControl.Invalidate();// 重绘}5. Channel 的优点对比你之前的 List lock特性ChannelList lock双缓冲环形缓冲区线程安全原生支持无需手动锁需手动加锁需少量锁需少量锁性能极高无锁算法中等锁竞争高高内存控制有界模式可限容易无限增长需手动管理固定内存背压机制支持满时阻塞或丢弃无无自动覆盖异步支持原生 async/await需手动同步为主同步为主适合实时波形★★★★★★★★★★★★★★★★★结论ChannelT是 .NET 官方推荐的现代生产者-消费者实现尤其适合高频数据采集 UI 实时显示场景。推荐组合使用用BoundedChannel防止内存溢出用TryWrite避免采集线程阻塞可接受少量丢帧UI 定期TryRead消费所有数据进行 AutoScale 和绘图如果你希望我基于ChannelT给你一个完整可替换原有类的版本也可以继续告诉我

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询