避免在释放控件时调用 Invoke

Posted

技术标签:

【中文标题】避免在释放控件时调用 Invoke【英文标题】:Avoid calling Invoke when the control is disposed 【发布时间】:2010-12-24 21:18:41 【问题描述】:

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

【问题讨论】:

为什么首先要处理它? @PoweRoy:我在控件的 Dispose 方法中通知线程退出。我知道这不是最好的做法,但我找不到更好的地方来指示线程退出。 【参考方案1】:

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

try

    if (mImageListView.InvokeRequired)
       mImageListView.Invoke(new YourDelegate(thisMethod));
    else
       mImageListView.RefreshInternal();
 
catch (ObjectDisposedException ex)

    // Do something clever

【讨论】:

我真的希望避免尝试/捕捉。但如果这是唯一的解决方案,我会这样做。 嗯,你可以通过使用互斥锁或锁来解决它,但它更容易出错,并且随着代码的发展可能导致奇怪的错误。您需要使用相同的互斥锁保护对 Dispose() 的所有调用,随着代码的发展,这将变得更加困难......【参考方案2】:

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

通过在 FormClosing 事件中设置 e.Cancel 并使用 ManualResetEvent 指示线程停止来实现。线程完成后,再次调用 Form.Close()。使用BackgroundWorker可以轻松实现线程完成逻辑,示例代码见this post。

【讨论】:

没有捕捉到ObjectDisposedException 异常,正如下面 Isak Savo 所说,比修补邪恶的 Thread.Abort() 或依赖 FormClosing 事件要干净得多? 你能保证它永远只是一个 ObjectDisposedException 吗?如果它是合法的 ObjectDisposedException 怎么办?这是一个兔子洞。解决问题,不要拍信使。 我最终做了类似的事情。但是我没有处理 FormClosing,而是覆盖了控件的 OnHandleDestroyed 并在等待线程退出时阻塞了 UI 线程。 这通常是导致死锁的原因,线程的完成回调无法运行,因为 UI 线程没有发送消息。听起来你设法避免了。 哎哟。继续敲击 X 以确保其正常工作。您的用户肯定会。【参考方案3】:

现实情况是,使用 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 消息队列,它将消息放入队列中以等待或触发并忘记跨线程调用,就像 Post 或 Send 消息一样。如果在 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 线程上的调用者,您将需要处理异常,并将其视为任何其他未成功的调用(以防止应用程序崩溃或做任何事情。除非要重写/ reroll 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)

    ...

可以根据需要定制异常过滤。很高兴知道,在大多数应用程序中,工作线程通常没有所有轻松的外部异常处理和记录 UI 线程所做的事情,因此您可能希望只吞噬工作人员端的任何异常。或者记录并重新抛出所有这些。对于许多工作线程上未捕获的异常意味着应用程序将崩溃。

【讨论】:

我真的希望有 TryInvokeTryBeginInvoke 方法用于对象尝试执行类似 Refresh 控件的情况,因为它的可见部分需要更新以反映新数据。所需的后置条件是控件没有任何仍需要更新的部分。控制的处置将意味着没有可见部分,因此没有过时的可见部分;因此,满足后置条件,方法应该成功返回。【参考方案4】:

尝试使用

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;
    

【讨论】:

+1 检查 IsHandleCreated 是否为真/假。这并不能解决竞争条件,但需要牢记。【参考方案5】:

可能是 lock(mImageListView)... ?

【讨论】:

这行不通。无法保证当您在锁内时不会调用 Disposed。 @Isak 锁的目的是阻止其他线程访问一个对象。如果他锁定了对象,那么根据定义,锁定时不能释放它。 您将对象锁定在 Thread2 上,但该对象在 Thread1 上被释放。因此,要么引入死锁,要么引入无操作。 @Jrud:这不是真的。锁只是意味着您阻止其他线程尝试获取相同的锁。仍然可以在“锁定”对象上调用任何方法。 可能只是阻止 GC 处理它,我想它应该在执行任何活动之前检查 SyncBlockIndex ob 对象的状态(以防用户代码未调用对象的 Dispose) 【参考方案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();

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

【讨论】:

IsDisposed 检查和 WaitOne 调用之间是否还存在竞争条件? 是的,你是对的。您可以在 WaitOne 调用之后对 IsDisposed 进行另一次检查,也可以将 WaitOne 放在 IsDisposed 检查之前。如果您在执行代码以节省额外调用时预计 IsDisposed 为 false 的次数多于 true 时,我会选择后一个选项。【参考方案7】:

另请参阅此问题:

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

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

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

【讨论】:

【参考方案8】:

如果可以使用 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");
    

【讨论】:

【参考方案9】:

处理表单关闭事件。检查您的非 UI 线程工作是否仍在进行,如果是,则开始将其关闭,取消关闭事件,然后使用表单控件上的 BeginInvoke 重新安排关闭。

private void Form_FormClosing(object sender, FormClosingEventArgs e)

    if (service.IsRunning)
    
        service.Exit();
        e.Cancel = true;
        this.BeginInvoke(new Action(() =>  this.Close(); ));
    

【讨论】:

【参考方案10】:

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

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

   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。

【讨论】:

我认为添加和删除事件处理程序并检查 isDisposed 是最常用的方法,但我不得不说。这对我来说非常有效。摆脱 try-catch 并且不必使用事件,所有这些都需要一个额外的 bool :-) 感谢您的提示!【参考方案11】:

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

if (mImageListView != null && 
    mImageListView.IsHandleCreated &&
    !mImageListView.IsDisposed)

    if (mImageListView.InvokeRequired)
        mImageListView.Invoke(new YourDelegate(thisMethod));
    else
        mImageListView.RefreshInternal();

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

【讨论】:

【参考方案12】:

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

【讨论】:

【参考方案13】:

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

public bool IsDisposed(Control ctrl)

    if (ctrl.IsDisposed)
        return true;
    try
    
        ctrl.Invoke(new Action(() =>  ));
        return false;
    
    catch (ObjectDisposedException)
    
        return true;
    

【讨论】:

【参考方案14】:

这对我有用

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

【讨论】:

以上是关于避免在释放控件时调用 Invoke的主要内容,如果未能解决你的问题,请参考以下文章

在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke

vb 释放Webbrowser控件

如何允许在回调时释放对象?

UIButton - 仅在没有待处理事件时调用 Release?

vb 释放Webbrowser控件

关于WPF中Image控件不释放内存的问题