BackgroundWorker возвращается к неправильному потоку

В моем приложении я создаю новый поток пользовательского интерфейса со следующим кодом:

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 не возвращается к правильному потоку пользовательского интерфейса, из которого он был запущен.

Может ли кто-нибудь помочь мне, что я могу сделать, чтобы решить эту проблему? Я не могу изменить код, который выполняет BackgroundWorker, потому что он находится в фреймворке, который я использую. Но я могу сделать что-то еще в RunWorkerCompleted-Event. Но я понятия не имею, как решить эту проблему.


person BennoDual    schedule 08.11.2011    source источник
comment
Зачем вы вообще создаете новый UI-Thread? Не нужно, не продуктивно и источник этой проблемы.   -  person Henk Holterman    schedule 09.11.2011


Ответы (6)


Проблема в том, что окно создается слишком рано. Поток еще не имеет контекста синхронизации. Вы можете увидеть, что это отладчик, установив точку останова при вызове конструктора BGW и взглянув на Thread.CurrentThread.ExecutionContext.SynchronizationContext. Это ноль. Это то, что BGW использует, чтобы решить, как маршалировать событие RunWorkerCompleted. В котором нет контекста синхронизации, событие выполняется в потоке пула потоков и вызывает гнев.

Вам нужно инициализировать диспетчер раньше. Не на 100% это правильный способ, но, похоже, он сработал:

        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);
    }
person Hans Passant    schedule 08.11.2011
comment
Или отложить создание бгв, теперь, когда я об этом думаю :) - person Hans Passant; 09.11.2011
comment
Вы не можете вызывать BeginInvoke таким образом, так как он будет продвигать создание окна в поток пользовательского интерфейса (Dispatcher.BeginInvoke - это диспетчер текущего окна, а не новый поток...) - person Reed Copsey; 09.11.2011
comment
Я думаю, это сработало бы, если бы вы использовали Dispatcher.CurrentDispatcher.BeginInvoke(...) - person Reed Copsey; 09.11.2011
comment
Да, этого я и опасался. Лучше удалите это. - person Hans Passant; 09.11.2011
comment
Я начал писать это - потом решил, что я должен проверить это, так я придумал свою версию... Только что попробовал, если вы используете Dispatcher.CurrentDispatcher.BeginInvoke, этот подход работает. - person Reed Copsey; 09.11.2011
comment
Привет, Ханс - это отлично работает - также в моем продуктивном приложении. Большое Вам спасибо. У меня есть еще один небольшой вопрос по этому поводу: не должен ли мой новый UI-Thread прерываться, когда я закрываю MyWindow? Должен ли я что-то изменить, чтобы поток был завершен после закрытия MyWindow? - person BennoDual; 09.11.2011

Столкнулся с похожей проблемой. На основе примечания 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)));
    }
}
person purvin    schedule 18.09.2012

Проблема в том, что вам нужно настроить файл SynchronizationContext. Обычно это не проблема, так как 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 будет на месте до создания окна.

person Reed Copsey    schedule 08.11.2011

Попробуйте указать getter и setter для BackgroundWorker внутри MyWindow. И передайте объект BackgroundWorker через метод установки в Mywindow. Это должно решить проблему, я думаю.

person Ravi Bhatt    schedule 08.11.2011

Вам нужно использовать метод делегата и вызов в вызывающей функции. Вот хороший пример: 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);
    }
}
person Jason    schedule 08.11.2011
comment
Это не должно быть необходимо. А в WPF это Dispatcher.Invoke() - person Henk Holterman; 09.11.2011

Я думаю, что простое перемещение кода настройки вашего фонового рабочего потока в событие «Загрузка» вместо конструктора должно быть очень хорошим.

person Eric Liprandi    schedule 08.11.2011