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

基于定时器更新集合时抛出“Collection was modified; enumeration operation may not execute.”异常

解决“Collection was modified”异常:ReactiveUI + Rx 进程集合更新问题

哥们,我太懂你这种困惑了——明明自己没在foreach的时候手动修改集合,结果还是炸出“Collection was modified; enumeration operation may not execute.”的异常。其实这事儿大概率是多线程并发操作搞的鬼,结合你用ReactiveUI和Rx的场景,咱们来拆解下根源和解决办法:

问题到底出在哪?

  • 线程冲突是核心:你的定时器更新逻辑(每200ms移除退出进程)大概率跑在后台线程,而DataGrid绑定的UI线程同时在枚举集合(比如渲染、排序的时候)。这时候两个线程一个改集合、一个枚举,哪怕你没在foreach代码块里改,枚举过程中其他线程动了集合,照样会触发这个异常。
  • Rx调度器的隐形坑:Rx的Interval默认用的是ThreadPool调度器,也就是说集合更新操作是在后台线程执行的;而UI绑定的集合通常是在UI线程访问,跨线程的并发访问直接就踩坑了。

针对性解决办法(按推荐程度排序)

1. 用线程安全集合直接规避冲突

把你当前用的普通集合(比如List<Process>)换成System.Collections.Concurrent里的线程安全集合,比如ConcurrentDictionary<int, Process>(用进程ID当Key,方便快速查找已退出的进程)。这类集合的枚举是快照式的,枚举时不会因为集合被修改而抛出异常,只会枚举到当时的快照内容。

示例代码:

// 定义线程安全的进程字典
private readonly ConcurrentDictionary<int, Process> _processStore = new ConcurrentDictionary<int, Process>();

// 给DataGrid绑定的只读属性
public IEnumerable<Process> Processes => _processStore.Values;

// 更新进程的逻辑
private void RefreshProcesses()
{
    var currentRunningProcesses = Process.GetProcesses();
    
    // 移除已退出的进程(先把Key转成List,避免枚举字典Key时被修改)
    foreach (var pid in _processStore.Keys.ToList())
    {
        if (!currentRunningProcesses.Any(p => p.Id == pid))
        {
            _processStore.TryRemove(pid, out _);
        }
    }
    
    // 添加新启动的进程
    foreach (var process in currentRunningProcesses)
    {
        _processStore.TryAdd(process.Id, process);
    }
    
    // 通知UI更新(ReactiveUI里用RaisePropertyChanged触发变更)
    this.RaisePropertyChanged(nameof(Processes));
}

2. 用Immutable集合适配Rx的函数式风格

ReactiveUI和Rx天生适配Immutable集合——因为Immutable集合的修改操作会返回全新的集合,原集合完全不会被改动,从根源上杜绝了并发修改的问题。

示例代码:

// 用ReactiveProperty持有Immutable进程列表
private readonly ReactiveProperty<ImmutableList<Process>> _processes = 
    new ReactiveProperty<ImmutableList<Process>>(ImmutableList<Process>.Empty);

// 给DataGrid绑定的属性
public IEnumerable<Process> Processes => _processes.Value;

// 构建Rx更新流
private void SetupProcessUpdateStream()
{
    this.WhenAnyValue(x => x.IsProcessTrackingChecked) // IsProcessTrackingChecked是你的复选框绑定属性
        .Where(isChecked => isChecked)
        .SwitchMap(_ => Observable.Interval(TimeSpan.FromMilliseconds(200))
            .SubscribeOn(RxApp.TaskpoolScheduler) // 后台线程获取进程列表,不卡UI
            .Select(_ => Process.GetProcesses().ToImmutableList())
            .ObserveOn(RxApp.MainThreadScheduler)) // 切回UI线程更新集合
        .Subscribe(newProcessList => _processes.Value = newProcessList);
}

这里的关键是:每次更新都是生成新的Immutable集合,原集合始终不变,UI线程枚举的时候永远不会遇到被修改的情况,而且ReactiveProperty会自动触发属性变更通知,DataGrid会自动刷新。

3. 强制集合操作在同一线程(适合传统集合)

如果你坚持用普通集合,那就必须保证所有对集合的修改和访问都在同一个线程(比如UI线程)。通过Rx的ObserveOn调度器就能实现:

private List<Process> _processes = new List<Process>();
public IEnumerable<Process> Processes => _processes;

private void SetupProcessUpdates()
{
    this.WhenAnyValue(x => x.IsChecked)
        .Where(isChecked => isChecked)
        .SwitchMap(_ => Observable.Interval(TimeSpan.FromMilliseconds(200))
            .SubscribeOn(RxApp.TaskpoolScheduler) // 后台线程拿进程数据
            .Select(_ => Process.GetProcesses().ToList())
            .ObserveOn(RxApp.MainThreadScheduler)) // 切回UI线程修改集合
        .Subscribe(currentProcesses => 
        {
            // UI线程是单线程的,修改时不会有其他UI操作在枚举集合
            _processes.Clear();
            _processes.AddRange(currentProcesses);
            this.RaisePropertyChanged(nameof(Processes));
        });
}

这个方法的核心是利用UI线程的单线程特性:在UI线程修改集合时,不会有其他UI线程的操作在枚举集合,自然就不会冲突。

额外提醒

  • 别直接在后台线程修改绑定到UI的集合,哪怕是ObservableCollection——它的CollectionChanged事件必须在UI线程触发,否则会抛出跨线程异常。
  • 如果你之前用ReactiveUI的ReactiveList,现在更推荐用ReactiveProperty配合Immutable集合,后者的线程安全性和函数式风格更适配Rx场景。

内容的提问来源于stack exchange,提问作者BertAR

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 10:49:36