C#线程间消息队列通信时WPF UI线程访问异常问题
问题:WPF线程通信更新UI时抛出InvalidOperationException异常
在学习线程技术并编写WPF应用时遇到以下问题:
- 最初因UI无法更新,使用
Task.Run开启工作线程处理文件夹结构读取任务 - 为解决
ConcurrentQueue轮询导致CPU占用过高的问题,改用ObservableConcurrentQueue并订阅其事件 - 更新
FolderCount时触发System.InvalidOperationException,异常提示:调用线程无法访问此对象,因为另一个线程拥有它 - 困惑点:原本认为事件处理程序会运行在UI主线程,实际却出现跨线程访问错误
相关代码片段:
{ public void SearchFolder(string path) { Task.Run(() => { SFolders(path); }); } private void SFolders(string path) { if (Directory.Exists(path)) { _pm.Add(ProcessMessages.FolderCountAdd); // 此处触发异常 // 其他文件夹处理逻辑 } // 其他代码 } } public partial class MainWindow : Window { public MainWindow mw = null; public ProcessMessages pm = null; public Searching searching = null; public MainWindow() { InitializeComponent(); mw = this; pm = new ProcessMessages(); searching = new Searching(pm, this); pm._queue.ContentChanged += OnObservableConcurrentQueueContentChanged; } private void OnObservableConcurrentQueueContentChanged(object sender, NotifyConcurrentQueueChangedEventArgs<int> args) { if (args.Action == NotifyConcurrentQueueChangedAction.Enqueue) { int message; pm.Fetch(out message); while (message > 0) { switch (message) { case ProcessMessages.FolderCountAdd: { FolderCount++; break; } case ProcessMessages.FileCountAdd: { FileCount++; break; } } pm.Fetch(out message); } } } public class ProcessMessages { public ObservableConcurrentQueue<int> _queue = new ObservableConcurrentQueue<int>(); public int Count => _queue.Count; public void Add(int message) { if (message == null) throw new ArgumentNullException(nameof(message)); _queue.Enqueue(message); } public bool Fetch(out int message) { return _queue.TryDequeue(out message); } } }
问题根源
ObservableConcurrentQueue的ContentChanged事件是在调用Enqueue的线程(也就是你开启的工作线程)上触发的,因此事件处理方法OnObservableConcurrentQueueContentChanged实际运行在工作线程中。而WPF的UI元素只能由创建它的主线程访问,直接在工作线程中修改绑定到UI的FolderCount属性,就会触发跨线程访问异常。
解决方法
方法1:在事件处理中切换到UI线程
利用WPF的Dispatcher将UI更新逻辑调度到主线程执行:
private void OnObservableConcurrentQueueContentChanged(object sender, NotifyConcurrentQueueChangedEventArgs<int> args) { if (args.Action == NotifyConcurrentQueueChangedAction.Enqueue) { // 切换到UI线程执行更新操作 Dispatcher.Invoke(() => { int message; pm.Fetch(out message); while (message > 0) { switch (message) { case ProcessMessages.FolderCountAdd: { FolderCount++; break; } case ProcessMessages.FileCountAdd: { FileCount++; break; } } pm.Fetch(out message); } }); } }
方法2:修改ObservableConcurrentQueue,在UI线程触发事件
如果使用的是自定义的ObservableConcurrentQueue,可以修改其事件触发逻辑,强制在UI线程发布事件:
public class ObservableConcurrentQueue<T> : ConcurrentQueue<T> { public event EventHandler<NotifyConcurrentQueueChangedEventArgs<T>> ContentChanged; public new void Enqueue(T item) { base.Enqueue(item); // 通过UI线程的Dispatcher触发事件 Application.Current.Dispatcher.Invoke(() => { ContentChanged?.Invoke(this, new NotifyConcurrentQueueChangedEventArgs<T>(NotifyConcurrentQueueChangedAction.Enqueue, item)); }); } }
额外优化点
ProcessMessages.Add方法中if (message == null)的判断无意义:int是值类型,不可能为null,可直接删除这段代码- 事件处理中的循环
Fetch逻辑可调整:每次Enqueue都会触发事件,重复循环可能导致消息重复处理,建议改为单次处理或优化队列消费逻辑
内容的提问来源于stack exchange,提问作者Dion
相关产品推荐
相关产品推荐

