Избегайте вызывать Invoke при удалении элемента управления

У меня есть следующий код в моем рабочем потоке (ImageListView ниже выводится из Control):

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

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

Ответ 1

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

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

Ответ 2

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

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

Ответ 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;
    }

Ответ 4

Реальность заключается в том, что с Invoke и друзьями вы не можете полностью защитить от 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)
{
    ...
}

Фундаментальная проблема:

При использовании средства Invoke используется очередь сообщений Windows, которая помещает сообщение в очередь либо ждать, либо запускать огонь и забывать о перекрестном потоке точно так же, как сообщение "Почта" или "Отправить". Если перед сообщением 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

Нет никакого реального способа узнать, что элемент управления собирается удалить себя из очереди, и ничего разумного вы не можете сделать, чтобы "отменить" вызов. Независимо от того, сколько проверок вы делаете или какие дополнительные блокировки вы делаете, вы не можете остановить кого-то другого, выдавая что-то вроде закрытия или деактивировать. Есть тонны сенариев, где это может произойти.

Решение:

Первое, что нужно понять, это то, что вызов будет терпеть неудачу, ничем не отличающийся от того, как проверка (IsHandleCreated) проигнорировала бы событие. Если целью является защита вызывающего абонента от потока, отличного от UI, вам нужно будет обрабатывать исключение и относиться к нему, как к любому другому вызову, который не удался (чтобы приложение не зависало или выполняло что угодно. И если вы не переписываете /reeroll 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)
{
    ...
}

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

Ответ 5

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

Ответ 6

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

Где-то в начале потока:

 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();

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

Ответ 7

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

Избежать проблем Invoke/BeginInvoke в обработке событий WinForm с несколькими потоками?

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

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

Ответ 8

Если BackGroundWorker является возможностью, существует очень простой способ простой, чтобы обойти это:

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");
    }
}

Ответ 9

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

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

Ответ 10

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

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

работает в С# 4.0, но по некоторым причинам он терпит неудачу в С# 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.

Ответ 11

Один из способов может состоять в том, чтобы вызвать сам метод, а не вызвать метод ImageListView:

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

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

Ответ 12

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