BackgroundWorker OnWorkCompleted выдает межпоточное исключение
-
03-07-2019 - |
Вопрос
У меня есть простой UserControl для подкачки базы данных, который использует контроллер для выполнения реальных вызовов DAL.Я использую BackgroundWorker
выполнять тяжелую работу и на OnWorkCompleted
если я снова включу некоторые кнопки, изменю TextBox.Text
свойство и вызвать событие для родительской формы.
Форма A содержит мой UserControl.Когда я нажимаю на какую-нибудь кнопку, открывающую форму Б, даже если я ничего «там» не делаю, а просто закрываю ее и пытаюсь загрузить следующую страницу из своей базы данных, OnWorkCompleted
вызывается в рабочем потоке (а не в моем основном потоке) и выдает межпотоковое исключение.
На данный момент я добавил проверку на InvokeRequired
в обработчике, но это не весь смысл OnWorkCompleted
должен быть вызван в главном потоке?Почему это не сработает так, как ожидалось?
РЕДАКТИРОВАТЬ:
Мне удалось сузить проблему до 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)
Решение
Это похоже на ошибку:
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()
Другие советы
Разве не вся суть
OnWorkCompleted
должен быть вызван в главном потоке?Почему это не сработает так, как ожидалось?
Нет, это не так.
Вы не можете просто запустить что-то старое в каком-то старом потоке.Потоки — это не вежливые объекты, которым можно просто сказать «запустите это, пожалуйста».
Лучшей мысленной моделью потока является грузовой поезд.Как только это произойдет, оно пойдет своим путем.Вы не можете изменить его курс или остановить его.Если вы хотите повлиять на него, вам придется либо подождать, пока он доберется до следующей железнодорожной станции (например:пусть он вручную проверит некоторые события) или пустит его под откос (Thread.Abort
и исключения CrossThread имеют почти те же последствия, что и сход поезда с рельсов...остерегаться!).
Элементы управления Winforms вроде поддерживают такое поведение (у них есть Control.BeginInvoke
который позволяет запускать любую функцию в потоке пользовательского интерфейса), но это работает только потому, что у них есть специальный перехватчик сообщений пользовательского интерфейса Windows и написаны некоторые специальные обработчики.Если использовать приведенную выше аналогию, их поезд проверяется на станции и периодически ищет новые направления, и вы можете использовать эту возможность, чтобы публиковать свои собственные маршруты.
А BackgroundWorker
предназначен для общего назначения (его нельзя привязать к графическому интерфейсу Windows), поэтому он не может использовать Windows Control.BeginInvoke
функции.Он должен предполагать, что ваш основной поток — это неостановимый «поезд», делающий свое дело, поэтому завершенное событие должно выполняться в рабочем потоке или не выполняться вообще.
Однако, поскольку вы используете winforms, в вашем OnWorkCompleted
обработчик, вы можете заставить окно выполнить другой обратный вызов с помощью 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 и Canceled перед доступом к свойству Result.Если возникло исключение или операция была отменена, доступ к свойству Result вызывает исключение.
А BackgroundWorker
проверяет, указывает ли экземпляр делегата на класс, который поддерживает интерфейс ISynchronizeInvoke
.Ваш уровень DAL, вероятно, не реализует этот интерфейс.Обычно вы используете BackgroundWorker
на Form
, который поддерживает этот интерфейс.
В случае, если вы хотите использовать BackgroundWorker
из уровня DAL и хотите обновить пользовательский интерфейс оттуда, у вас есть три варианта:
- ты бы продолжал звонить
Invoke
метод - реализовать интерфейс
ISynchronizeInvoke
в классе DAL и перенаправить вызовы вручную (всего три метода и свойство) - прежде чем вызывать
BackgroundWorker
(итак, в потоке пользовательского интерфейса), чтобы вызватьSynchronizationContext.Current
и сохранить экземпляр содержимого в переменной экземпляра.АSynchronizationContext
затем даст вамSend
метод, который будет делать именно то, чтоInvoke
делает.
Лучший способ избежать проблем с перекрестной многопоточностью в графическом интерфейсе — это используйте SynchronizationContext.