2009-12-09 4 views
23

я следующий код в моем рабочем потоке (ImageListView ниже происходит от Control):Избегайте вызова Invoke, когда элемент управления расположен

if (mImageListView != null && 
    mImageListView.IsHandleCreated && 
    !mImageListView.IsDisposed) 
{ 
    if (mImageListView.InvokeRequired) 
     mImageListView.Invoke(
      new RefreshDelegateInternal(mImageListView.RefreshInternal)); 
    else 
     mImageListView.RefreshInternal(); 
} 

Однако я получаю ObjectDisposedException иногда с Invoke выше способом. Похоже, что управление может быть установлено между временем, которое я проверяю IsDisposed, и я звоню Invoke. Как я могу избежать этого?

+0

Почему это расположено в первую очередь? – RvdK

+0

@PoweRoy: Я сигнализирую о том, что потоки выходят в методе Dispose элемента управления. Я знаю, что это не лучшая вещь, но я не мог найти лучшего места, чтобы сигнализировать о выходе потоков. –

ответ

13

В вашем коде есть неявные условия гонки. Элемент управления может быть удален между вашим тестом IsDisposed и тестом InvokeRequired. Между InvokeRequired и Invoke() есть еще один. Вы не можете исправить это, не контролируя жизнь потока. Учитывая, что ваш поток генерирует данные для представления списка, он должен перестать работать до исчезновения списка.

Сделайте это, установив e.Cancel в событии FormClosing и сообщив о прекращении потока с помощью ManualResetEvent. Когда поток завершится, снова вызовите Form.Close(). Thread.Abort() - это второй выбор, но гораздо проще реализовать. Использование BackgroundWorker упрощает реализацию логики завершения потока.

+0

Не поймает исключение 'ObjectDisposedException', как указано ниже Исаком Саво, намного чище, чем возиться с нечестивым Thread.Abort() или полагаться на событие FormClosing? – arul

+2

Можете ли вы гарантировать, что это будет только объект ObjectDisposedException? Что, если это законное исключение ObjectDisposedException? Это кроличья дыра. Устраните проблему, не стреляйте в посланника. –

+0

В итоге я сделал что-то похожее на это. Но вместо того, чтобы обращаться с FormClosing, я перегрузил элемент управления OnHandleDestroyed и заблокировал поток пользовательского интерфейса, ожидая выхода потоков. –

0

Одним из способов может быть вызвать сам метод из них более вместо вызова ImageListView-метод:

if (mImageListView != null && 
    mImageListView.IsHandleCreated && 
    !mImageListView.IsDisposed) 
{ 
    if (mImageListView.InvokeRequired) 
     mImageListView.Invoke(new YourDelegate(thisMethod)); 
    else 
     mImageListView.RefreshInternal(); 
} 

Таким образом, было бы проверить еще раз, прежде чем, наконец, вызывая RefreshInternal().

1

может быть блокировкой (mImageListView) {...}?

+0

Это не сработает. Нет никакой гарантии, что Disposed не вызывается, когда вы находитесь внутри замка. –

+0

+1 за то, что я сказал. – Jrud

+0

@Isak Цель блокировки - заблокировать другие потоки от доступа к объекту. Если он блокирует объект, то по определению он не может быть удален во время блокировки. – Jrud

16

У вас есть race condition. Вам лучше просто поймать исключение ObjectDisposed и сделать с ним. На самом деле, я думаю, что в данном случае это только рабочее решение.

try 
{ 
    if (mImageListView.InvokeRequired) 
     mImageListView.Invoke(new YourDelegate(thisMethod)); 
    else 
     mImageListView.RefreshInternal(); 
} 
catch (ObjectDisposedException ex) 
{ 
    // Do something clever 
} 
+0

Я действительно надеялся избежать попытки/ловли. Но я сделаю это, если это единственное решение. –

+0

Ну, вы можете * решить его с помощью мьютекса или блокировок, но он гораздо более подвержен ошибкам и может привести к странным ошибкам по мере развития кода. Вам нужно будет защитить все вызовы Dispose() с помощью одного и того же мьютекса, и это будет усложняться по мере развития кода ... –

1

Вы можете использовать мьютексы.

Где-то в начале резьбы:

Mutex m=new Mutex(); 

Тогда:

if (mImageListView != null && 
    mImageListView.IsHandleCreated && 
    !mImageListView.IsDisposed) 
{ 
    m.WaitOne(); 

    if (mImageListView.InvokeRequired) 
     mImageListView.Invoke(
      new RefreshDelegateInternal(mImageListView.RefreshInternal)); 
    else 
     mImageListView.RefreshInternal(); 

    m.ReleaseMutex(); 
} 

И везде, где это вы выбываете из mImageListView:

m.WaitOne(); 
mImageListView.Dispose(); 
m.ReleaseMutex(); 

Это должно гарантировать, что Вы наклоняете выставлять и вызывать одновременно.

+0

Не существует ли еще условия гонки между проверкой IsDisposed и вызовом WaitOne? –

+0

Да, хорошо заметили, вы правы. Вы можете либо поставить еще одну проверку на IsDisposed после вызова WaitOne, либо вы можете поставить WaitOne перед проверкой IsDisposed. Я бы выбрал последний вариант, если вы ожидаете, что IsDisposed будет ложным больше раз, чем true, когда код выполняется для сохранения дополнительного вызова. –

1

Смотрите также вопрос:

Avoiding the woes of Invoke/BeginInvoke in cross-thread WinForm event handling?

Служебный класс, в результате EventHandlerForControl может решить эту проблему для сигнатур методов событий. Вы можете адаптировать этот класс или пересмотреть логику, чтобы решить эту проблему.

Настоящая проблема заключается в том, что nobugz является правильным, поскольку он указывает, что API-адреса, предоставленные для вызовов с перекрестными потоками в winforms, по своей сути не являются потокобезопасными. Даже внутри вызовов InvokeRequired и Invoke/BeginInvoke есть несколько условий гонки, которые могут вызвать неожиданное поведение.

1

Если BackgroundWorker возможность, есть очень simple способ обойти это:

public partial class MyForm : Form 
{ 
    private void InvokeViaBgw(Action action) 
    { 
     BGW.ReportProgress(0, action); 
    } 

    private void BGW_ProgressChanged(object sender, ProgressChangedEventArgs e) 
    { 
     if (this.IsDisposed) return; //You are on the UI thread now, so no race condition 

     var action = (Action)e.UserState; 
     action(); 
    } 

    private private void BGW_DoWork(object sender, DoWorkEventArgs e) 
    { 
     //Sample usage: 
     this.InvokeViaBgw(() => MyTextBox.Text = "Foo"); 
    } 
} 
1

Обрабатывать событие закрытия формы. Проверьте, не прекращается ли ваша работа с пользовательским интерфейсом, если это так начинает отменять, отмените событие закрытия и затем перенести завершение с помощью BeginInvoke в элементе управления формой.

private void Form_FormClosing(object sender, FormClosingEventArgs e) 
{ 
    if (service.IsRunning) 
    { 
     service.Exit(); 
     e.Cancel = true; 
     this.BeginInvoke(new Action(() => { this.Close(); })); 
    } 
} 
1

Решение, предложенное Исака Саво

try 
    { 
    myForm.Invoke(myForm.myDelegate, new Object[] { message }); 
    } 
catch (ObjectDisposedException) 
    { //catch exception if the owner window is already closed 
    } 

работает в C# 4.0, но по некоторым причинам он не в C# 3.0 (исключение возникает в любом случае)

Так что я использовал другое решение на основе флага, указывающего, закрывается ли форма и, следовательно, предотвращает использование вызова, если флаг установлен

public partial class Form1 : Form 
    { 
    bool _closing; 
    public bool closing { get { return _closing; } } 

    private void Form1_FormClosing(object sender, FormClosingEventArgs e) 
    { 
     _closing = true; 
    } 

... 

// part executing in another thread: 

if (_owner.closing == false) 
    { // the invoke is skipped if the form is closing 
    myForm.Invoke(myForm.myDelegate, new Object[] { message }); 
    } 

Это имеет то преимущество, что вы избегаете использования try/catch.

+0

Я думаю, что добавление и удаление обработчиков событий и проверка isDisposed - самый используемый способ, но я должен сказать. Это отлично сработало для меня. Избавиться от try-catch и не использовать события, все за стоимость одного дополнительного bool :-) Спасибо за подсказку! –

3

Попробуйте использовать

if(!myControl.Disposing) 
    ; // invoke here 

У меня была точно такая же проблема, как вы. С тех пор, как я переключился на проверку. Утилизация объекта ObjectDisposedException удалена. Не сказав, что это исправит его в 100% случаев, всего 99%;) Есть еще шанс на состояние гонки между проверкой на Disposing и вызовом для вызова, но в тесте, которое я сделал, я не побежал (я использую ThreadPool и рабочий поток).

Вот что я использую перед каждым вызовом для вызова:

private bool IsControlValid(Control myControl) 
    { 
     if (myControl == null) return false; 
     if (myControl.IsDisposed) return false; 
     if (myControl.Disposing) return false; 
     if (!myControl.IsHandleCreated) return false; 
     if (AbortThread) return false; // the signal to the thread to stop processing 
     return true; 
    } 
+0

+1 для проверки правильности/false IsHandleCreated. Это не фиксирует состояние гонки, но что-то нужно иметь в виду. – TamusJRoyce

0

Предложение, чтобы остановить поток генерации сообщений не является приемлемым. Делегаты могут быть многоадресными. Поскольку один слушатель не хочет слушать группу, вы не снимаете участников группы. Поскольку фреймворк не обеспечивает простой способ, я знаю, как очистить сообщение от сообщений этих сообщений о событиях, а так как форма не раскрывает его частную собственность, которая позволяет нам знать, что форма закрывается: Установите флаг на IsClosing Событие окна после того, как вы отмените подписку или прекратите прослушивание событий, и всегда проверяйте этот флаг, прежде чем делать это. Invoke().

2

Реальность заключается в том, что с помощью Invoke и друзей вы не можете полностью защитить от вызова на удаленном компоненте, а затем получить InvalidOperationException из-за отсутствия дескриптора. Я havnt действительно видел ответ еще, как тот, который находится ниже, в любом из потоков, который затрагивает фундаментальную проблему, которая не может быть полностью решена путем выборочного тестирования или использования семантики блокировки.

Вот нормальная «правильная» идиома:

// the event handler. in this case preped for cross thread calls 
void OnEventMyUpdate(object sender, MyUpdateEventArgs e) 
{ 
    if (!this.IsHandleCreated) return; // ignore events if we arn't ready, and for 
             // invoke if cant listen to msg queue anyway 
    if (InvokeRequired) 
     Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData); 
    else 
     this.MyUpdate(e.MyData); 
} 

// the update function 
void MyUpdate(Object myData) 
{ 
    ... 
} 

fundemental проблема:

При использовании объекта Invoke используется очередь окна сообщения, которое помещает сообщение в очередь либо ждать, либо запускать огонь и забывать о перекрестном потоке точно так же, как сообщение «Почта» или «Отправить».Если перед сообщением Invoke появляется сообщение, которое приведет к аннулированию компонента и его дескриптора окна или которое будет помещено сразу после любых проверок, которые вы пытаетесь выполнить, тогда у вас будет плохое время.

x thread -> PostMessage(WM_CLOSE); // put 'WM_CLOSE' in queue 
y thread -> this.IsHandleCreated  // yes we have a valid handle 
y thread -> this.Invoke();   // put 'Invoke' in queue 
ui thread -> this.Destroy();   // Close processed, handle gone 
y thread -> throw Invalid....()  // 'Send' comes back, thrown on calling thread y 

Там нет реального способа узнать, что управление собирается удалить себя FROMTHE очереди, а на самом деле ничего разумного вы можете сделать, чтобы «отменить» ВЫЗОВ. Независимо от того, сколько проверок вы делаете или какие дополнительные блокировки вы делаете, вы не можете остановить кого-то другого, выдавая что-то вроде закрытия или деактивировать. Есть тонны сенариев, где это может произойти.

Решение:

Первое, что нужно понимать, что Invoke собирается на провал, не отличается от того, как (IsHandleCreated) чек проигнорировали бы это событие. Если целью является защита вызывающего абонента от потока, отличного от UI, вам нужно будет обрабатывать исключение и относиться к нему, как к любому другому вызову, который не удался (чтобы приложение не зависало или выполняло что угодно. И если вы не переписываете/перебросить Invoke объекта, улов ваш единственный способ узнать.

// the event handler. in this case preped for cross thread calls 
void OnEventMyWhatever(object sender, MyUpdateEventArgs e) 
{ 
    if (!this.IsHandleCreated) return; 
    if (InvokeRequired) 
    { 
     try 
     { 
      Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData); 
     } 
     catch (InvalidOperationException ex) // pump died before we were processed 
     { 
      if (this.IsHandleCreated) throw; // not the droids we are looking for 
     } 
    } 
    else 
    { 
     this.MyUpdate(e.MyData); 
    } 
} 

// the update function 
void MyUpdate(Object myData) 
{ 
    ... 
} 

фильтрации исключение может быть адаптирована в соответствии с какими бы ни были потребности. Его хорошо, чтобы быть в курсе, что рабочие потоки часто не имеют все тепленькое внешнюю обработку исключений и протоколирование потоков пользовательского интерфейса выполняется в большинстве приложений, поэтому вы можете просто сожрать любое исключение на стороне рабочего. Или запишите и реконструируйте все из них. Для многих исключенных исключений для рабочего потока означает, что приложение будет аварийно завершено.

+1

Мне очень жаль, что не было методов TryInvoke и TryBeginInvoke для ситуаций, когда объект пытается сделать что-то вроде 'Refresh', потому что его видимые части нужно обновлять, чтобы отражать новые данные. Желательным пост-условием является то, что у элемента управления нет никаких частей, которые все еще нуждаются в обновлении. Утилизация элемента управления будет означать, что нет видимых частей и, следовательно, нет устаревших видимых частей; следовательно, пост-условие будет выполнено, и метод должен успешно вернуться. – supercat

0

У меня такая же ошибка. моя ошибка произошла в потоке. наконец, я пишу этот метод:

public bool IsDisposed(Control ctrl) 
{ 
    if (ctrl.IsDisposed) 
     return true; 
    try 
    { 
     ctrl.Invoke(new Action(() => { })); 
     return false; 
    } 
    catch (ObjectDisposedException) 
    { 
     return true; 
    } 
} 
Смежные вопросы