在处理控件时避免调用Invoke

我的工作线程中有以下代码(下面的ImageListView是从Control派生的):

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

但是,我有时会使用上面的Invoke方法获得ObjectDisposedException 。 看来控件可以在我检查IsDisposed和我调用Invoke 。 我怎么能避免这种情况?

您的代码中存在隐含的竞争条件。 控件可以在IsDisposed测试和InvokeRequired测试之间进行处理。 InvokeRequired和Invoke()之间还有另一个。 如果不确保控件的使用寿命超过线程的寿命,则无法解决此问题。 鉴于您的线程正在为列表视图生成数据,它应该在列表视图消失之前停止运行。

通过在FormClosing事件中设置e.Cancel并通过ManualResetEvent发出停止信号来执行此操作。 线程完成后,再次调用Form.Close()。 使用BackgroundWorker可以轻松实现线程完成逻辑,在本文中查找示例代码。

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

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

尝试使用

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

我和你有完全相同的问题。 自从我切换到检查.Disposing控件后,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; } 

实际情况是,使用Invoke和朋友,您无法完全防止对已处置组件的调用,或者由于缺少句柄而导致InvalidOperationException。 我还没有真正看到答案,如下面的那个,在解决真正基本问题的任何线程中,这些问题不能通过抢先测试或使用锁语义完全解决。

这是正常的’正确’成语:

 // 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线程上的调用者,则需要处理exception,并将其视为未成功的任何其他调用(以防止应用程序崩溃或执行任何操作。除非要重写/ reroll Invoke facility,catch是你唯一知道的方法。

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

可以定制exception过滤以满足任何需求。 很高兴知道工作线程通常没有所有轻松的外部exception处理和记录UI线程,在大多数应用程序中,所以你可能希望只是吞噬工作者端的任何exception。 或者记录并重新抛出所有这些。 对于许多人来说,工作线程上未被捕获的exception意味着应用程序将崩溃。

可能是锁(mImageListView){…}?

你可以使用互斥锁。

在线程开头的某个地方:

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

这应该确保您不能同时处置和调用。

另见这个问题:

在跨线程WinForm事件处理中避免Invoke / BeginInvoke的困境?

产生EventHandlerForControl的实用程序类可以解决事件方法签名的这个问题。 您可以调整此课程或查看其中的逻辑来解决问题。

这里真正的问题是nobugz是正确的,因为他指出在winforms中为跨线程调用提供的API本质上不是线程安全的。 即使在对InvokeRequired和Invoke / BeginInvoke本身的调用中,也存在一些可能导致意外行为的竞争条件。

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

处理表格结束活动。 检查你的关闭UI线程是否仍在工作,如果是这样,开始关闭它,取消关闭事件,然后使用窗体控件上的BeginInvoke重新安排关闭。

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

Isak Savo提出的解决方案

 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中失败(无论如何都会引发exception)

所以我使用另一个解决方案,该解决方案基于一个标志,指示表单是否正在关闭,因此如果设置了标志则阻止使用调用

  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的优点。

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

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

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

停止生成消息的线程的建议是不可接受的。 代表可以进行多播。 因为一个听众不想听乐队,所以你不要拍乐队成员。 由于框架没有提供任何简单的方法来清除这些事件消息的消息泵,并且由于表单没有公开其私有属性,让我们知道表单正在关闭:在IsClosing事件上设置一个标志取消订阅或停止侦听事件后的窗口,并在执行此操作之前始终检查此标志.Invoke()。

我有同样的错误。 我的错误发生在线程中。 最后我写了这个方法:

 public bool IsDisposed(Control ctrl) { if (ctrl.IsDisposed) return true; try { ctrl.Invoke(new Action(() => { })); return false; } catch (ObjectDisposedException) { return true; } } 

这对我有用

 if (this.IsHandleCreated){ Task.Delay(500).ContinueWith(_ =>{ this.Invoke(fm2); }); } else { this.Refresh(); }