处理UI线程和Backgroundworker的问题
处理UI线程和Backgroundworker的问题
我试图实现的目标很简单。我有一个动态计时器(用户可以更改的计时器),它调用后台工作程序来获取用户的外部IP地址。 计时器
和 后台工作程序
的组合导致了一些问题。以下是代码:
namespace IPdevices { ////// Interaction logic for Main.xaml /// public partial class Main : Window { private readonly BackgroundWorker worker; private IPret iprep; private Timer timer; public Main(Client client) { InitializeComponent(); iprep = new IPret(); startClock(); worker = new BackgroundWorker(); worker.DoWork += worker_DoWork; worker.RunWorkerCompleted += worker_RunWorkerCompleted; worker.WorkerReportsProgress = true; worker.ProgressChanged += worker_ProgressChanged; } private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { ipAdd.Content = e.UserState; } private void startClock() { timer = new Timer(); timer.Interval = 2000; timer.Elapsed += new ElapsedEventHandler(clockTimer_Tick); timer.Start(); } private void clockTimer_Tick(object sender, ElapsedEventArgs e) { timer.Stop(); worker.RunWorkerAsync(); } private void worker_DoWork(object sender, DoWorkEventArgs e) { Console.WriteLine("Checking ip"); iprep.refresh(); worker.ReportProgress(0, iprep.getExternalIp()); Console.WriteLine("Found ip"); } private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { timer.Start(); } } }
基本上,一旦计时器触发,我希望获取IP地址并在应用程序的标签上输出。然而,在ProgressChanged
方法中,我遇到了一个异常,说它无法更改,因为另一个线程拥有它。那个线程是哪一个?是由另一个线程拥有的iprep
吗?实际上,RunWorkerCompleted
从未触发。我很难理解哪个线程拥有什么以及对象如何被锁定...任何洞察力都将不胜感激。
问题出现的原因是由于UI线程和后台线程之间的交叉线程操作。BackgroundWorker在一个与UI线程不同的线程上运行,如果想要修改UI元素的属性,需要在UI线程上进行操作。一种解决方法是使用Dispatcher.Invoke,但是在WPF中有更好的方法。
解决方法是使用MVVM设计模式,将后台代码移到ViewModel中。然后可以这样做:
string _XXContent; public string XXContent { get { return _XXContent; } set { _XXContent = value; OnPropertyChanged("XXContent"); } } private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { XXContent = e.UserState; }
xaml:
如果使用的是C# 5,则还应该研究一下async/IProgress,摒弃BackgroundWorker。
我也会研究一下这个问题。感谢您的建议。与WPF一起工作确实是一次过山车之旅。
我对此的唯一问题是,您向别人推荐了MVVM,但没有推荐ASYNC和IProgress。我强烈建议放弃BackgroundWorker,将方法转换为async方法,并传递一个带有IProgress的委托。这样在ViewModel中的代码看起来更清晰,也更容易调试和编写单元测试。
问题的出现原因:UI线程的任务和后台任务没有正确分离,导致UI线程被阻塞。
解决方法:使用DispatcherTimer类来执行UI线程的任务,使用async和await关键字来实现后台任务的异步执行,确保UI线程不被阻塞。
以下是修复后的代码:
private void StartClock() { var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; timer.Tick += async (o, e) => await GetIP(); timer.Start(); } private async Task GetIP() { Debug.WriteLine("Checking ip"); await Task.Run(() => { // 在此异步获取IP }); Debug.WriteLine("Found ip"); // 在此更新UI }
这段代码通过在UI线程中执行Tick处理程序,并异步运行后台操作,确保不会阻塞UI线程。修复后的代码解决了UI线程和后台任务之间的问题,使得程序能够正确运行。
问题的出现原因是Timer在WPF中的行为与WinForms中不同,Timer的Tick事件不会在UI线程上发生,而是在Timer自己的线程上发生。因此,在Tick事件中访问UI元素会导致错误。
解决方法是使用Application.Current.Dispatcher.BeginInvoke方法将工作委托到UI线程上执行。这样可以保证工作是在与点击按钮或通过其他用户操作触发时相同的线程上启动的。
另外,还可以使用DispatcherTimer替代Timer,因为DispatcherTimer的Tick处理程序会在UI线程上执行,不需要使用BeginInvoke方法。此外,还可以将BackgroundWorker替换为使用async和await关键字的异步任务。
通过这些解决方法,可以避免在Tick事件中访问UI元素时出现错误,并且可以使用更现代的异步编程模型来处理后台任务。
这篇文章介绍了在WPF中使用Timer和BackgroundWorker时可能遇到的问题,并提供了解决方法。通过这些方法,可以确保在Tick事件中访问UI元素时不会出现错误,并可以使用更好的异步编程模型来处理后台任务。