基于定时器更新集合时抛出“Collection was modified; enumeration operation may not execute.”异常
哥们,我太懂你这种困惑了——明明自己没在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

