多线程循环任务中JSON序列化时如何避免集合修改异常?
咱们先把问题根源戳透:你用的List<T>本身不是线程安全的,多个线程一边往里面添加LoadedJockey元素,一边Newtonsoft.Json在序列化时要枚举整个集合——这俩并发操作撞在一起,就会触发那个「Collection was modified; enumeration operation may not execute」异常。
下面给你三个可行的解决方案,从省心到兼容现有代码,你可以根据自己的场景选:
方案1:改用线程安全集合(最推荐,改动最小)
直接把_allJockeys的类型从List<LoadedJockey>换成ConcurrentBag<LoadedJockey>。这是.NET专门为多线程添加元素的场景设计的集合,它的枚举操作会自动生成集合的只读快照——也就是说,序列化的时候只会处理快照里的内容,哪怕其他线程同时在添加元素,也不会干扰序列化过程。
修改后的关键代码:
// 先把_allJockeys的定义改成ConcurrentBag private ConcurrentBag<LoadedJockey> _allJockeys = new ConcurrentBag<LoadedJockey>(); // 任务里添加元素的地方不用改,直接用Add(ConcurrentBag的Add是线程安全的) if (jockey.Name != null) { _allJockeys.Add(jockey); } // 保存方法只需把参数类型改成ConcurrentBag即可 public void SaveAllJockeys(ConcurrentBag<LoadedJockey> allJockeys) { if (allJockeys.Count != 0) { if (File.Exists(_jockeysFileName)) File.Delete(_jockeysFileName); try { using (StreamWriter file = File.CreateText(_jockeysFileName)) { JsonSerializer serializer = new JsonSerializer(); serializer.Serialize(file, allJockeys); } } catch (Exception e) { dialog.ShowDialog("Could not save the results, " + e.ToString(), "Error"); } } }
方案2:给集合操作加互斥锁(兼容现有List)
如果不想替换集合类型,那就给所有访问、修改_allJockeys的地方加锁,确保同一时间只有一个线程能操作集合——不管是添加元素还是序列化。
首先定义一个专用的锁对象:
// 定义一个全局锁对象,确保所有操作都用同一个锁 private readonly object _jockeysLock = new object();
然后修改任务中的添加逻辑:
if (jockey.Name != null) { // 加锁后再添加元素,避免并发修改 lock (_jockeysLock) { _allJockeys.Add(jockey); } }
最后修改保存方法,序列化前先加锁:
public void SaveAllJockeys(List<LoadedJockey> allJockeys) { if (allJockeys.Count != 0) { if (File.Exists(_jockeysFileName)) File.Delete(_jockeysFileName); try { // 加锁确保序列化过程中集合不会被其他线程修改 lock (_jockeysLock) { using (StreamWriter file = File.CreateText(_jockeysFileName)) { JsonSerializer serializer = new JsonSerializer(); serializer.Serialize(file, allJockeys); } } } catch (Exception e) { dialog.ShowDialog("Could not save the results, " + e.ToString(), "Error"); } } }
方案3:序列化前创建集合快照(折中方案)
还有一种思路:在保存时先创建集合的一个副本(快照),然后序列化这个副本。但要注意,创建快照的过程也必须加锁,不然复制到一半集合被修改,还是会触发异常。
修改后的保存方法:
public void SaveAllJockeys(List<LoadedJockey> allJockeys) { if (allJockeys.Count != 0) { if (File.Exists(_jockeysFileName)) File.Delete(_jockeysFileName); try { List<LoadedJockey> snapshot; // 加锁创建快照,确保复制过程中集合状态不变 lock (_jockeysLock) { snapshot = allJockeys.ToList(); } // 序列化快照,这时候原集合就算被修改也不影响序列化 using (StreamWriter file = File.CreateText(_jockeysFileName)) { JsonSerializer serializer = new JsonSerializer(); serializer.Serialize(file, snapshot); } } catch (Exception e) { dialog.ShowDialog("Could not save the results, " + e.ToString(), "Error"); } } }
小提示
- 优先选方案1,因为
ConcurrentBag是.NET官方优化的线程安全集合,代码改动最少,也不用手动管理锁的逻辑。 - 如果必须保留
List<T>,方案2是最稳妥的,因为锁能完全避免并发冲突;方案3适合序列化耗时较长的场景,能减少锁的持有时间。
内容的提问来源于stack exchange,提问作者bakunet
相关产品推荐
相关产品推荐

