UI 线程和 Backgroundworker 出现问题
Trouble with UI Threads and Backgroundworker
我要实现的目标很简单。我有一个动态计时器(一个可以由用户更改的计时器),它调用后台工作人员去获取用户的外部 IP 地址。 Timer
和 BackgroundWorker
的组合导致了一些问题。这是代码:
namespace IPdevices
{
/// <summary>
/// Interaction logic for Main.xaml
/// </summary>
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
永远不会被解雇。我无法理解哪些线程拥有什么以及对象如何被锁定...任何见解将不胜感激。
如果我没记错的话,ipAdd 是一个 UI 元素。如果是,那么问题出在交叉线程上。
发生的事情是后台工作人员将 运行 在与 UI 线程不同的线程上。如果你想修改 UI 元素的 属性,你需要在 UI 线程上进行。一种选择是使用 Dispatcher.Invoke
但由于您使用的是 WPF,因此有更好的方法来实现。
搜索一下 MVVM 设计模式并将后台代码移到视图模型中。然后你可以做类似
的事情
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 :
<TextBox Content={Binding XXContent}/>
编辑:
如果您使用的是 c# 5,那么您应该查看 async/IProgress 以及摆脱后台工作者。
这似乎在我的测试中修复了它
private void clockTimer_Tick(object sender, ElapsedEventArgs e)
{
timer.Stop();
Action a = () =>
{
worker.RunWorkerAsync();
};
Application.Current.Dispatcher.BeginInvoke(a);
}
此外,我会注意到这是 WPF 中 Timer 的一致行为(我以前没有在 WPF 中使用过它);在 clockTimer_Tick
中尝试 ipAdd.Content = "Tick";
会导致相同的错误。 System.Timers.Timer 的 tick 事件不会在 UI 线程上发生。
用下面显示的几行代码替换您的所有代码。 Tick
处理程序在 UI 线程中执行。它仍然异步运行后台操作并且不会阻塞 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(() =>
{
// Get the IP asynchronously here
});
Debug.WriteLine("Found ip");
// Update the UI here
}
我要实现的目标很简单。我有一个动态计时器(一个可以由用户更改的计时器),它调用后台工作人员去获取用户的外部 IP 地址。 Timer
和 BackgroundWorker
的组合导致了一些问题。这是代码:
namespace IPdevices
{
/// <summary>
/// Interaction logic for Main.xaml
/// </summary>
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
永远不会被解雇。我无法理解哪些线程拥有什么以及对象如何被锁定...任何见解将不胜感激。
ipAdd 是一个 UI 元素。如果是,那么问题出在交叉线程上。
发生的事情是后台工作人员将 运行 在与 UI 线程不同的线程上。如果你想修改 UI 元素的 属性,你需要在 UI 线程上进行。一种选择是使用 Dispatcher.Invoke
但由于您使用的是 WPF,因此有更好的方法来实现。
搜索一下 MVVM 设计模式并将后台代码移到视图模型中。然后你可以做类似
的事情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 :
<TextBox Content={Binding XXContent}/>
编辑: 如果您使用的是 c# 5,那么您应该查看 async/IProgress 以及摆脱后台工作者。
这似乎在我的测试中修复了它
private void clockTimer_Tick(object sender, ElapsedEventArgs e)
{
timer.Stop();
Action a = () =>
{
worker.RunWorkerAsync();
};
Application.Current.Dispatcher.BeginInvoke(a);
}
此外,我会注意到这是 WPF 中 Timer 的一致行为(我以前没有在 WPF 中使用过它);在 clockTimer_Tick
中尝试 ipAdd.Content = "Tick";
会导致相同的错误。 System.Timers.Timer 的 tick 事件不会在 UI 线程上发生。
用下面显示的几行代码替换您的所有代码。 Tick
处理程序在 UI 线程中执行。它仍然异步运行后台操作并且不会阻塞 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(() =>
{
// Get the IP asynchronously here
});
Debug.WriteLine("Found ip");
// Update the UI here
}