BackgroundWorker OnWorkCompleted создает исключение для нескольких потоков - PullRequest
3 голосов
/ 04 мая 2009

У меня есть простой UserControl для подкачки базы данных, который использует контроллер для выполнения реальных вызовов DAL. Я использую BackgroundWorker для выполнения тяжелой работы, и в случае события OnWorkCompleted я снова включаю некоторые кнопки, изменяю свойство TextBox.Text и вызываю событие для родительской формы.

Форма A содержит мой UserControl. Когда я нажимаю на какую-то кнопку, которая открывает форму B, даже если я ничего не делаю «там», просто закрываю ее и пытаюсь вывести следующую страницу из моей базы данных, OnWorkCompleted вызывается в рабочем потоке ( а не мой главный поток), и выкидывает исключение кросс-потока.

В данный момент я добавил проверку для InvokeRequired в обработчик, но разве не весь смысл OnWorkCompleted должен вызываться в главном потоке? Почему бы не работать так, как ожидалось?

EDIT:

Мне удалось сузить проблему до Arcgis и BackgroundWorker. У меня есть следующее решение, которое добавляет команду в arcmap, которая открывает простой Form1 с двумя кнопками.

Первая кнопка запускает BackgroundWorker, который спит в течение 500 мс и обновляет счетчик. В методе RunWorkerCompleted он проверяет InvokeRequired и обновляет заголовок, чтобы показать, когда метод первоначально выполнялся внутри основного потока или рабочего потока. Вторая кнопка просто открывает Form2, которая ничего не содержит.

Сначала все вызовы RunWorkerCompletedare выполняются в основном потоке (как и ожидалось - это точка зрения метода RunWorkerComplete, по крайней мере, как я понимаю из MSDN на BackgroundWorker)

После открытия и закрытия Form2, RunWorkerCompleted всегда вызывается в рабочем потоке. Я хочу добавить, что я могу просто оставить это решение проблемы как есть (проверьте InvokeRequired в методе RunWorkerCompleted), но я хочу понять, почему это происходит вопреки моим ожиданиям. В моем «реальном» коде я бы всегда хотел знать, что метод RunWorkerCompleted вызывается в главном потоке.

Мне удалось определить проблему с помощью команды form.Show(); в моем BackgroundTesterBtn - если вместо этого я использую ShowDialog(), у меня не возникнет проблем (RunWorkerCompleted всегда запускается в основном потоке). Мне нужно использовать Show() в моем проекте ArcMap, чтобы пользователь не был привязан к форме.

Я также пытался воспроизвести ошибку в обычном проекте WinForms. Я добавил простой проект, который просто открывает первую форму без ArcMap, но в этом случае я не смог воспроизвести ошибку - RunWorkerCompleted выполнялся в главном потоке, независимо от того, использовал ли я Show() или ShowDialog(), до и после открытие Form2. Я пытался добавить третью форму, чтобы она действовала как основная форма перед моим Form1, но это не изменило результат.

Здесь - это мой простой sln (VS2005sp1) - требуется

ESRI.ArcGIS.ADF (9.2.4.1420)

ESRI.ArcGIS.ArcMapUI (9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

Ответы [ 4 ]

7 голосов
/ 04 мая 2009

Разве весь смысл OnWorkCompleted не вызывается в главном потоке? Почему бы не сработать, как ожидалось?

Нет, это не так.
Вы не можете просто запустить любую старую вещь в любом старом потоке. Потоки не являются вежливыми объектами, которые можно просто сказать «запустите это, пожалуйста».

Лучшей мысленной моделью нити является грузовой поезд. Как только это идет, это идет на его собственном следе. Вы не можете изменить его курс или остановить его. Если вы хотите повлиять на него, вы должны либо подождать, пока он доберется до следующей железнодорожной станции (например: вручную проверить некоторые события), либо сорвать его (Thread.Abort и исключения CrossThread имеют почти те же последствия, что и выход из строя. поезд ... остерегайтесь!).

Элементы управления Winforms своего рода поддерживают это поведение (у них есть Control.BeginInvoke, который позволяет запускать любую функцию в потоке пользовательского интерфейса), но это работает только потому, что у них есть специальный хук в насос сообщений пользовательского интерфейса Windows и написать несколько специальных обработчиков. Чтобы воспользоваться приведенной выше аналогией, их поезд регистрируется на станции и периодически ищет новые направления, и вы можете использовать это средство для публикации своих собственных указаний.

BackgroundWorker предназначен для общего назначения (его нельзя привязать к графическому интерфейсу Windows), поэтому он не может использовать функции Windows Control.BeginInvoke. Он должен предполагать, что ваш основной поток - это неостановимый «поезд», делающий свое дело, поэтому завершенное событие должно запускаться в рабочем потоке или не выполняться вовсе.

Однако, поскольку вы используете winforms, в вашем обработчике OnWorkCompleted вы можете заставить Window выполнить другой обратный вызов с использованием функции BeginInvoke, о которой я упоминал выше. Как это:

// Assume we're running in a windows forms button click so we have access to the 
// form object in the "this" variable.
void OnButton_Click(object sender, EventArgs e )
    var b = new BackgroundWorker();
    b.DoWork += ... blah blah

    // attach an anonymous function to the completed event.
    // when this function fires in the worker thread, it will ask the form (this)
    // to execute the WorkCompleteCallback on the UI thread.
    // when the form has some spare time, it will run your function, and 
    // you can do all the stuff that you want
    b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); }
    b.RunWorkerAsync(); // GO!
}

void WorkCompleteCallback()
{
    Button.Enabled = false;
    //other stuff that only works in the UI thread
}

Также не забудьте:

Ваш обработчик событий RunWorkerCompleted должен всегда проверять свойства Error и Cancelled перед обращением к свойству Result. Если возникло исключение или операция была отменена, доступ к свойству Result вызывает исключение.

2 голосов
/ 22 мая 2009

Похоже, ошибка:

http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930

http://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx

Поэтому я предлагаю использовать пуленепробиваемый (псевдокод):

if(control.InvokeRequired)
  control.Invoke(Action);
else
  Action()
2 голосов
/ 04 мая 2009

BackgroundWorker проверяет, указывает ли экземпляр делегата на класс, который поддерживает интерфейс ISynchronizeInvoke. Ваш уровень DAL, вероятно, не реализует этот интерфейс. Обычно вы используете BackgroundWorker на Form, который поддерживает этот интерфейс.

Если вы хотите использовать BackgroundWorker из слоя DAL и хотите обновить пользовательский интерфейс оттуда, у вас есть три варианта:

  • вы бы по-прежнему вызывали Invoke метод
  • реализует интерфейс ISynchronizeInvoke в классе DAL и перенаправляет вызовы вручную (это только три метода и свойство)
  • перед вызовом BackgroundWorker (то есть в потоке пользовательского интерфейса), чтобы вызвать SynchronizationContext.Current и сохранить экземпляр содержимого в переменной экземпляра. Затем SynchronizationContext даст вам метод Send, который точно сделает то, что делает Invoke.
1 голос
/ 04 мая 2009

Лучший способ избежать проблем с многопоточностью в GUI - использовать SynchronizationContext .

...