C#实时图表更新遇Collection修改异常,ToList()无法解决
我用C#写了个从CSV读数据画图表的程序,CSV文件由Windows服务每35ms写入一次。程序每秒读取文件更新数据列表,实现每秒刷新的实时图表。一次性读取全部数据时图表正常,但定时读取更新列表时,立刻抛出“Collection was modified; enumeration operation may not execute”异常。已经在foreach里加了.ToList(),但问题还是没解决。
相关代码
public Form1() { InitializeComponent(); this.Load += Form1_Load; } System.Timers.Timer timer_Chart_update = new System.Timers.Timer(); private void Form1_Load(object sender, EventArgs e) { this.Controls.Add(chart2); Init_Chart_Update_Timer(); } public static List<double> List_Position = new List<double>(); public static List<double> List_Current = new List<double>(); public static List<double> List_X_Axis = new List<double>(); public void Init_Chart_Update_Timer() { timer_Chart_update.Elapsed += new ElapsedEventHandler(OnTimedEvent); timer_Chart_update.Interval = 1000; timer_Chart_update.Start(); } private void OnTimedEvent(object sender, EventArgs e) { Read_Test_File(); Draw_Chart(); } public void Read_Test_File() { List_Position.Clear(); try { using (var fs = new FileStream("D:\\ConCheetah\\Test4\\test_files\\test_01.csv", FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var streamReader = new StreamReader(fs, Encoding.UTF8, true)) { String line; while ((line = streamReader.ReadLine()) != null) { if (line == "$") { break; } bool isLetter = !String.IsNullOrEmpty(line) && Char.IsLetter(line[0]); if (isLetter == true) { continue; } List_Position.Add(double.Parse(line)); } } } catch(Exception e) { using (var writer = new StreamWriter(@"D:\ConCheetah\test4\log6.txt", true)) { writer.WriteLine("File Read Error " + e+ " "+ DateTime.Now.ToString()); } } } public void Draw_Chart() { chart2.Series.Clear(); chart2.Titles.Clear(); chart2.Legends.Clear(); var newSeries_1 = new Series(); var newSeries_2 = new Series(); newSeries_1.ChartType = SeriesChartType.Line; newSeries_2.ChartType = SeriesChartType.Line; chart2.Series.Add(newSeries_1); chart2.Series.Add(newSeries_2); List_X_Axis.Clear(); for (int w = 0; w <= 940; w++) { List_X_Axis.Add(w); } var t = chart2.Titles.Add("Title1"); t.Text = " Position/Current"; t.ForeColor = Color.Silver; chart2.ChartAreas[0].AxisX.Title = "."; chart2.ChartAreas[0].AxisY.Title = "mm"; chart2.ChartAreas[0].AxisY.TitleForeColor = Color.Orange; chart2.ChartAreas[0].AxisY.LabelStyle.ForeColor = Color.Orange; chart2.ChartAreas[0].AxisX.Minimum = 0d; chart2.Series[0].Color = Color.Orange; chart2.ChartAreas[0].AxisY.Minimum = 0; chart2.ChartAreas[0].AxisY.Maximum = 15; chart2.ChartAreas[0].AxisY2.Enabled = AxisEnabled.True; chart2.ChartAreas[0].AxisY2.Minimum = -350; chart2.ChartAreas[0].AxisY2.Maximum = 350; chart2.ChartAreas[0].AxisY2.Title = "% of Max. Current"; chart2.ChartAreas[0].AxisY2.TitleForeColor = Color.Yellow; chart2.ChartAreas[0].AxisY2.LabelStyle.ForeColor = Color.Yellow; chart2.ChartAreas[0].AxisY2.MajorGrid.LineColor = Color.Silver; chart2.ChartAreas[0].AxisY2.MajorTickMark.LineColor = Color.Silver; chart2.Series[1].YAxisType = AxisType.Secondary; chart2.Series[1].Color = Color.Yellow; chart2.ChartAreas[0].BackColor = Color.Black; int x = 1; foreach (float v in List_Position.ToList()) { newSeries_1.Points.AddXY(x, v); x++; } int x2 = 1; foreach (float v in List_Position.ToList()) { newSeries_2.Points.AddXY(x2, v); x2++; } }
异常堆栈信息
System.InvalidOperationException: 集合已修改;枚举操作可能无法执行。
at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
at System.Collections.Generic.List1.Enumerator.MoveNextRare() at System.Collections.Generic.List1.Enumerator.MoveNext()
at System.Windows.Forms.DataVisualization.Charting.Series.ResetAutoValues(Boolean reset)
at System.Windows.Forms.DataVisualization.Charting.Series.UnPrepareData(ISite controlSite)
at System.Windows.Forms.DataVisualization.Charting.Data.DataManager.ChartPicture_AfterPaint(Object sender, ChartPaintEventArgs e)
at System.Windows.Forms.DataVisualization.Charting.ChartPicture.OnAfterPaint(ChartPaintEventArgs e)
1. 解决线程冲突问题
System.Timers.Timer的Elapsed事件在非UI线程触发,直接操作WinForms控件和共享集合会导致线程安全问题——图表在UI线程绘制时,定时器线程可能正在修改相关集合。
修改OnTimedEvent方法,切换到UI线程执行:
private void OnTimedEvent(object sender, EventArgs e) { if (this.InvokeRequired) { this.Invoke(new Action(() => { Read_Test_File(); Draw_Chart(); })); } else { Read_Test_File(); Draw_Chart(); } }
2. 给集合操作加锁保证原子性
静态共享集合List_Position的读写操作可能并发执行,加锁避免同时修改和读取:
// 定义全局锁对象 private static readonly object _lockObj = new object(); public void Read_Test_File() { lock(_lockObj) { List_Position.Clear(); try { // 原读取逻辑不变 } catch(Exception e) { // 原异常处理不变 } } } public void Draw_Chart() { // ... 原初始化代码不变 ... lock(_lockObj) { // 复制一份集合,避免绘制时原集合被修改 var positionCopy = new List<double>(List_Position); int x = 1; foreach (var v in positionCopy) { newSeries_1.Points.AddXY(x, v); x++; } int x2 = 1; foreach (var v in positionCopy) { newSeries_2.Points.AddXY(x2, v); x2++; } } }
3. 改用WinForms专用定时器
直接用System.Windows.Forms.Timer,它的Tick事件在UI线程触发,从根源避免线程切换问题:
// 替换原定时器声明 private System.Windows.Forms.Timer timer_Chart_update = new System.Windows.Forms.Timer(); public void Init_Chart_Update_Timer() { timer_Chart_update.Tick += OnTimedEvent; timer_Chart_update.Interval = 1000; timer_Chart_update.Start(); } // 修改事件方法签名 private void OnTimedEvent(object sender, EventArgs e) { Read_Test_File(); Draw_Chart(); }
关键说明
异常堆栈显示问题出在图表控件的AfterPaint阶段,说明绘制时相关集合(比如Series的Points或绑定的数据源)被修改了。线程同步和集合操作的原子性是解决问题的核心,即使加了.ToList(),如果原集合在复制后被修改,图表控件内部仍可能引用原集合进行计算,导致异常。
内容的提问来源于stack exchange,提问作者Mdarende

