BackgroundWorker返回到错误的线程

3
在我的应用程序中,我使用以下代码创建了一个新的UI-Thread:
Thread thread = new Thread(() =>
    {
        MyWindow windowInAnotherThread = new MyWindow();
        windowInAnotherThread.Show();
        System.Windows.Threading.Dispatcher.Run();
    }) { IsBackground = true };
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();

这给我带来了以下的问题:
在MyWindow类的构造函数中,一个BackgroundWorker被执行。在RunWorkerCompleted中应该使用一些数据更新一个控件,这些数据是BackgroundWorker正在计算的。
我建立了一个小示例,用于说明此问题:
public partial class MyWindow : Window {
    public MyWindow() {
        InitializeComponent();

        var bw = new BackgroundWorker();
        bw.DoWork += bw_DoWork;
        bw.RunWorkerCompleted += bw_RunWorkerCompleted;
        bw.RunWorkerAsync();
    }

    void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
        this.Title = "Calculated title";
    }

    void bw_DoWork(object sender, DoWorkEventArgs e) {
        Thread.Sleep(3000);
    }
}

bw_RunWorkerCompleted() 中,我遇到了一个 InvalidOperationException 错误(调用线程无法访问此对象,因为不同的线程拥有它)。看起来,BackgroundWorker 没有返回到它启动的正确 UI 线程。
请问有谁能帮我解决这个问题吗?我无法更改执行 BackgroundWorker 的代码,因为它是一个我使用的框架中的代码。但是我可以在 RunWorkerCompleted 事件中进行其他操作。但我不知道如何解决这个问题。

为什么你要“创建一个新的UI线程”呢?这并不是必要的,也不是高效的做法,而且还会导致问题的产生。 - H H
6个回答

4
问题在于窗口创建得太早了。线程尚未拥有同步上下文。您可以在BGW构造函数调用上设置断点,并查看Thread.CurrentThread.ExecutionContext.SynchronizationContext,在调试器中可以看到它是空的。这就是BGW用来决定如何传递RunWorkerCompleted事件的方式。如果没有同步上下文,则该事件会在线程池线程上运行,这可能导致问题。
您需要更早地初始化分发程序。我不确定这是否是正确的方法,但它似乎有效:
        Thread thread = new Thread(() => {
            System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => {
                MyWindow windowInAnotherThread = new MyWindow();
                windowInAnotherThread.Show();
            }));
            System.Windows.Threading.Dispatcher.Run();
        }) { IsBackground = true };
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

你还需要显式地强制关闭线程。请将以下方法添加到MyWindow中:
    protected override void OnClosed(EventArgs e) {
        Dispatcher.BeginInvokeShutdown(System.Windows.Threading.DispatcherPriority.Background);
    }

或者,现在我想想,延迟创建bgw :) - Hans Passant
你不能这样调用BeginInvoke,因为它会将窗口创建推入UI线程中(Dispatcher.BeginInvoke是当前窗口的调度程序,而不是新线程的调度程序...)。 - Reed Copsey
如果你使用 Dispatcher.CurrentDispatcher.BeginInvoke(...),我认为这个方法会起作用。 - Reed Copsey
是的,这正是我担心的。最好删除它。 - Hans Passant
1
我开始写这个 - 然后想测试一下,这就是我想出自己版本的方式... 我刚试了一下,如果你使用 Dispatcher.CurrentDispatcher.BeginInvoke,这种方法是可行的。 - Reed Copsey
嗨Hans - 这个很好用 - 在我的生产应用中也很好用。非常感谢你。我还有一个小问题:当我关闭MyWindow时,我的新UI线程不应该被终止吗?我需要改变一些东西,才能让线程在关闭MyWindow之后终止吗? - BennoDual

2
我遇到了类似的问题。根据下面的注释1和注释2,我创建了UIBackgroundWorker。也许这可以帮助其他遇到这个问题的开发者。
如果它有效,请告诉我,或者更新设计以造福其他开发者。
public class UIBackgroundWorker : BackgroundWorker
{

    private System.Windows.Threading.Dispatcher uiDispatcher;
    public SafeUIBackgroundWorker(System.Windows.Threading.Dispatcher uiDispatcher)
        : base()
    {
        if (uiDispatcher == null)
            throw new Exception("System.Windows.Threading.Dispatcher instance required while creating UIBackgroundWorker");
        else
            this.uiDispatcher = uiDispatcher;
    }

    protected override void OnProgressChanged(ProgressChangedEventArgs e)
    {
        if (uiDispatcher.CheckAccess())
            base.OnProgressChanged(e);
        else
            uiDispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => base.OnProgressChanged(e)));
    }

    protected override void OnRunWorkerCompleted(RunWorkerCompletedEventArgs e)
    {
        if (uiDispatcher.CheckAccess())
            base.OnRunWorkerCompleted(e);
        else
            uiDispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => base.OnRunWorkerCompleted(e)));
    }
}

1
问题在于您需要设置“同步上下文”。通常情况下这不是问题,因为“Dispatcher.Invoke”会为您设置它,但由于您在构造函数中使用了“BackgroundWorker”(在“Dispatcher.Run”之前触发),因此没有设置上下文。
将线程创建更改为:
Thread thread = new Thread(() =>
    {
        // Create the current dispatcher (done via CurrentDispatcher)
        var dispatcher = Dispatcher.CurrentDispatcher;
        // Set the context
        SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(dispatcher));

        MyWindow windowInAnotherThread = new MyWindow();
        windowInAnotherThread.Show();
        Dispatcher.Run();
    });

thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();

这将使其正常运行,因为在窗口构建之前将会有一个SynchronizationContext

0

你需要在调用函数中使用委托方法和调用。这里有一个很好的例子:http://msdn.microsoft.com/en-us/library/aa288459(v=vs.71).aspx

使用你的代码,

    public partial class MyWindow : Window {


    delegate void TitleSetter(string title);

    public MyWindow() {
            InitializeComponent();

        var bw = new BackgroundWorker();
        bw.DoWork += bw_DoWork;
        bw.RunWorkerCompleted += bw_RunWorkerCompleted;
        bw.RunWorkerAsync();
    }

    void SetTitle(string T)
    {
      this.Title = T;
    }

    void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {

      try    
        {
        TitleSetter T = new TitleSetter(SetTitle);
        invoke(T, new object[]{"Whatever the title should be"}); //This can fail horribly, need the try/catch logic.
        }catch (Exception){}
    }

    void bw_DoWork(object sender, DoWorkEventArgs e) {
        Thread.Sleep(3000);
    }
}

这不应该是必要的。在WPF中,它是Dispatcher.Invoke() - H H

0
尝试为你的BackgroundWorker提供gettersetter,并将BackgroundWorker对象通过setter方法传递给MyWindow。这样应该就可以解决问题了,我想。

0

我认为将后台工作线程设置代码简单地移动到“Load”事件中而不是构造函数中应该就可以了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接