You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程循环任务中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:39:30