为什么' RunWorkerCompleted'在错误的线程上执行?

时间:2014-09-01 12:57:06

标签: c# .net multithreading winforms backgroundworker

在以下代码中,启动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);
}

有人可以解释一下这里发生了什么吗?

2 个答案:

答案 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()方法就是某个地方。这样做是为了在表单继续在桌面上生活时,创建线程可以自由终止。