28

我的工作线程中有以下代码(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。我怎样才能避免这种情况?

4

14 回答 14

19

你在这里有一个竞争条件。您最好只捕获 ObjectDisposed 异常并完成它。事实上,我认为在这种情况下,这是唯一可行的解​​决方案。

try
{
    if (mImageListView.InvokeRequired)
       mImageListView.Invoke(new YourDelegate(thisMethod));
    else
       mImageListView.RefreshInternal();
} 
catch (ObjectDisposedException ex)
{
    // Do something clever
}
于 2009-12-09T15:47:56.613 回答
14

There are implicit race conditions in your code. The control can be disposed between your IsDisposed test and the InvokeRequired test. There's another one between InvokeRequired and Invoke(). You can't fix this without ensuring the control outlives the life of the thread. Given that your thread is generating data for a list view, it ought to stop running before the list view disappears.

Do so by setting e.Cancel in the FormClosing event and signaling the thread to stop with a ManualResetEvent. When the thread completes, call Form.Close() again. Using BackgroundWorker makes it easy to implement the thread completion logic, find sample code in this post.

于 2009-12-09T15:52:23.320 回答
4

The reality is that with Invoke and friends, you can't completely protect against invoke on a disposed component, or then getting InvalidOperationException because of the missing handle. I haven't really seen an answer yet, like the one farther below, in any of the threads that addresses the real fundamental problem, which cant be completely solved by preemptive testing or using lock semantics.

Here's the normal 'correct' idiom:

// 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)
{
    ...
}

The fundemental problem:

In using the Invoke facility the windows message queue is used, which places a message in the queue to either wait or fire-and-forget the cross thread call exactly like Post or Send message. If there is a message ahead of the Invoke message that will invalidate the component and its window handle, or that got placed just after any checks you try to perform, then you are going to have a bad time.

 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

There is no real way to know that the control is about to remove itself fromthe queue, and nothing really reasonable you can do to "undo" the invoke. No matter how many checks you do or extra locks you make, you cant stop someone else form issuing something like a close, or deactivate. There are tons of senarios where this can happen.

A solution:

The first thing to realize is that the invoke is going to fail, no different than how a (IsHandleCreated) check would have ignored the event. If the goal is to protect the caller on the non-UI thread you will need to handle the exception, and treat it like any other call that didn't succeed (to keep app from crashing or do whatever. And unless going to rewrite/reroll Invoke facility, the catch is your only way to know.

// 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)
{
    ...
}

The exception filtering can be tailored to suit whatever the needs are. Its good to be aware that worker threads often dont have all the cushy outer exception handling and logging the UI threads do, in most applicaitons, so you may wish to just gobble up any exception on the worker side. Or log and rethrow all of them. For many, uncaught exceptions on worker thread means the app is going to crash.

于 2013-09-05T22:41:40.680 回答
3

Try using

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

I had the exact same problem as you. Ever since I switched to checking .Disposing on the control, the ObjectDisposedException has gone away. Not saying this will fix it 100% of the time, just 99% ;) There is still a chance of a race condition between the check to Disposing and the call to invoke, but in the testing I've done I haven't ran into it (I use the ThreadPool and a worker thread).

Here's what I use before each call to invoke:

    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;
    }
于 2012-10-30T16:44:00.337 回答
1

可能是 lock(mImageListView){...} ?

于 2009-12-09T15:45:09.320 回答
1

You could use mutexes.

Somewhere at the start of the thread :

 Mutex m=new Mutex();

Then :

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

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

    m.ReleaseMutex();
}

And whereever it is you are disposing of mImageListView :

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

This should ensure you cant dispose and invoke at the same time.

于 2009-12-09T15:55:15.310 回答
1

See also this question:

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

The utility class that resulted EventHandlerForControl can solve this problem for event method signatures. You could adapt this class or review the logic therein to solve the issue.

The real problem here is that nobugz is correct as he points out that the APIs given for cross-thread calls in winforms are inherently not thread safe. Even within the calls to InvokeRequired and Invoke/BeginInvoke themselves there are several race conditions that can cause unexpected behavior.

于 2009-12-09T17:04:51.140 回答
1

If a BackGroundWorker is a possibility, there's a very simple way to circumvent this:

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");
    }
}
于 2010-11-15T23:55:34.797 回答
1

Handle the Form closing event. Check to see if your off UI thread work is still happening, if so start to bring it down, cancel the closing event and then reschedule the close using BeginInvoke on the form control.

private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
    if (service.IsRunning)
    {
        service.Exit();
        e.Cancel = true;
        this.BeginInvoke(new Action(() => { this.Close(); }));
    }
}
于 2011-09-06T21:05:09.350 回答
1

The solution proposed by Isak Savo

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

works in C# 4.0 but for some reasons it fails in C#3.0 (the exception is raised anyway)

So I used another solution based on a flag indicating if the form is closing and consequently preventing the use of invoke if the flag is set

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

This has the advantage of completely avoiding the use of try/catch.

于 2011-10-16T12:17:37.593 回答
0

一种方法可能是更多地调用方法本身,而不是调用 ImageListView-Method:

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

这样,它会在最终调用 RefreshInternal() 之前再检查一次。

于 2009-12-09T15:43:07.810 回答
0

The suggestion to stop the thread generating the messages is not acceptable. Delegates can be multicast. Because one listener does not want to listen to the band, you don't shoot the band members. Since the framework doesn't provide any easy way I know of to clear the message pump of those event messages, and since the form does not expose its private property that lets us know the form is closing: Set a flag on the IsClosing Event of the window after you unsubscribe or stop listening to the events, and always check this flag before you do a this.Invoke().

于 2013-06-28T15:56:15.033 回答
0

i have same error. my error occurred in thread. finally i write this method :

public bool IsDisposed(Control ctrl)
{
    if (ctrl.IsDisposed)
        return true;
    try
    {
        ctrl.Invoke(new Action(() => { }));
        return false;
    }
    catch (ObjectDisposedException)
    {
        return true;
    }
}
于 2018-02-20T10:54:03.863 回答
0

This works for me

if (this.IsHandleCreated){
    Task.Delay(500).ContinueWith(_ =>{
        this.Invoke(fm2);
    });
} else {
  this.Refresh();
}
于 2018-03-14T00:35:32.553 回答