线程安全致TextProgress与TextMax不一致,寻求更优锁实现方案
解决多线程下ViewModel属性的线程安全问题
你的问题核心在于TextProgress++并非原子操作——它包含读取当前值、加1、写回新值三个独立步骤,多线程并发执行时会出现竞态条件,导致最终数值和预期的TextMax不符。不想锁定整个ViewModel、也不愿为每个属性单独建类的话,这里有几个更优雅的解决方案:
方案1:使用Interlocked类做原子操作
对于简单的整数自增/自减操作,.NET提供的System.Threading.Interlocked类能保证这些操作的原子性,无需显式加锁,性能也更优。
实现方式:
在ResultsItemViewModel中封装线程安全的增量方法,把原子操作的逻辑隐藏在内部:
public class ResultsItemViewModel : INotifyPropertyChanged { int _textProgress, _textMax; // ... 原有属性和NotifyPropertyChanged代码保持不变 ... // 新增线程安全的增量方法 public void IncrementTextProgress() { // 原子性地自增_textProgress,并获取新值 int newValue = Interlocked.Increment(ref _textProgress); // WPF中需确保属性变更通知在UI线程触发 Application.Current.Dispatcher.Invoke(() => NotifyPropertyChanged(nameof(TextProgress))); } }
然后在Method1和Method2中,替换原来的_ViewModel.TextProgress++为:
_ViewModel.IncrementTextProgress();
这样既避免了竞态条件,又不需要锁定整个ViewModel,粒度精准到单个属性的操作。
方案2:为单个属性分配专属锁对象
如果你的属性操作不止简单自增(比如需要做复杂的数值计算),可以为每个需要线程安全的属性单独创建一个私有的锁对象,锁的粒度足够细,不会影响其他属性的操作。
实现方式:
public class ResultsItemViewModel : INotifyPropertyChanged { int _textProgress, _textMax; // 为TextProgress分配专属锁对象 private readonly object _textProgressLock = new object(); // ... 原有属性和NotifyPropertyChanged代码保持不变 ... public void IncrementTextProgress() { lock (_textProgressLock) { _textProgress++; NotifyPropertyChanged(nameof(TextProgress)); } } }
外部调用同样是_ViewModel.IncrementTextProgress(),这样锁只会作用于TextProgress相关的操作,其他属性的更新不受影响。
方案3:结合IProgress<T>做线程安全的进度报告
你原来的方法里有reportProgress参数,其实可以利用IProgress<T>来统一处理进度更新,它会自动把更新调度到UI线程(创建时捕获当前同步上下文),同时也能避免竞态条件:
实现方式:
private async void Button_Click(object sender, RoutedEventArgs e) { _ViewModel.TextProgress = 0; // 创建Progress对象,自动绑定UI线程上下文 var progress = new Progress<int>(value => _ViewModel.TextProgress = value); await TextProcessing(progress); lblResult.Content = _ViewModel.TextProgress + "/" + _ViewModel.TextMax; } // 修改TextProcessing和Method1/Method2,传入进度实例 private async Task TextProcessing(IProgress<int> progress) { int totalProgress = 0; await Task.WhenAll( Task.Run(() => Method1(val => Interlocked.Add(ref totalProgress, val))), Task.Run(() => Method2(val => Interlocked.Add(ref totalProgress, val)))); // 最后统一更新UI进度 progress.Report(totalProgress); } private void Method1(Action<int> reportProgress = null) { for (int i = 0; i < 10000; i++) { reportProgress?.Invoke(1); } } // Method2做同样修改
这个方案更适合需要批量汇总多线程进度的场景,同时天然保证了UI线程的安全性。
选择建议
- 如果只是简单的整数自增/自减,优先用方案1(Interlocked),性能最优且代码简洁;
- 如果有复杂的属性操作(比如多步计算后更新),用方案2(专属锁对象),灵活且粒度可控;
- 如果需要和UI更新强绑定,或者要汇总多个线程的进度,用**方案3(IProgress
)**更合适。
内容的提问来源于stack exchange,提问作者windowsgm
相关产品推荐
相关产品推荐

