在以下代码中,启动BackgroundWorker
时,SynchronizationContext
确实存在,但仍然会执行 RunWorkerCompleted 处理程序与RunWorkerAsync()
不同的线程因此抛出异常。为什么呢?
当删除对tempForm
的调用时,它运行正常。 (当MessageBox
代替Form
时,也是如此。
(代码显示Form
,在一秒后启动引用另一个表单 f1 的BackgroundWorker
,然后显示第二个表单 f1 即可。)子>
public static Form1 f1;
static BackgroundWorker worker = new BackgroundWorker();
[STAThread]
static void Main()
{
worker.DoWork += worker_DoWork;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
f1 = new Form1();
using (Form1 tempForm = new Form1()) tempForm.ShowDialog();
//MessageBox.Show("A MessageBox won't cause the exception later. Only the Form does.");
if (SynchronizationContext.Current == null) throw new Exception("This is NOT thrown");
worker.RunWorkerAsync();
Application.Run(f1);
}
static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show(f1, "Inside RunWorkerCompleted");
//Throws: Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.
}
static void worker_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(1000);
}
有人可以解释一下这里发生了什么吗?
答案 0 :(得分:6)
问题是因为您从默认同步上下文中调用RunWorkerAsync
。举个小例子:
public static void Main()
{
var ctx1 = SynchronizationContext.Current; // returns null
var form = new Form();
var ctx2 = SynchronizationContext.Current; // returns a WindowsFormsSyncContext
form.ShowDialog();
var ctx3 = SynchronizationContext.Current; // returns a SynchronizationContext
worker.RunWorkerAsync(); // wrong context now
}
实例化表单似乎将WindowsFormsSynchronizationContext
与当前线程相关联。有趣的是,在关闭表单后,关联的同步上下文将被设置为默认值,即使用线程池的那个。
经过一番挖掘后,我发现了 - 乍一看 - 奇怪行为的原因:Control
的构造函数在必要时初始化WindowsFormsSynchronizationContext
(参见reference source)。从ShowDialog
返回后,就不会有任何消息循环,因此必须重置SynchronizationContext.Current
,在这种情况下必须重置为默认的线程池SynchronizationContext
。
答案 1 :(得分:0)
Windows UI不是线程安全的,根本不支持多线程。出于这个原因,检查哪个线程创建并稍后尝试操纵分配的图形资源。为了避免异常,您必须使用此处显示的调用模式:
if(InvokeRequired)
{
Invoke(worker_RunWorkerCompleted, sender, e);
}
else
{
MessageBox.Show(f1, "Inside RunWorkerCompleted");
}
不同的线程运行该方法的事实是正常的。 Windows窗体由必须重入的进入线程构成,这意味着您不应该首先阻塞(无限循环)运行该程序的线程。
如果仔细观察,Main()
中的Run()
方法就是某个地方。这样做是为了在表单继续在桌面上生活时,创建线程可以自由终止。