在跨线程WinForm事件处理中避免Invoke / BeginInvoke的困扰? [英] Avoiding the woes of Invoke/BeginInvoke in cross-thread WinForm event handling?

查看:124
本文介绍了在跨线程WinForm事件处理中避免Invoke / BeginInvoke的困扰?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我仍然在WinForm UI中的背景线程困扰。为什么?以下是一些问题:


  1. 显然是最重要的问题,我无法修改控件,除非我在同一个线程上执行创建它。

  2. 如你所知,Invoke,BeginInvoke等在创建控件之后不可用。

  3. 即使在RequiresInvoke返回true之后BeginInvoke仍然可以抛出ObjectDisposed,即使没有抛出,如果控件被销毁,它也可能永远不会执行代码。

  4. 即使在RequiresInvoke返回true之后,Invoke可以无限期挂起由执行调用与Invoke调用同时处理的控件执行。

我正在寻找一个优雅的解决方案这个问题,但在我详细了解我正在寻找的内容之前,我以为我会澄清这个问题。这是为了解决通用问题,并提出一个更具体的例子。对于这个例子,假设我们正在通过互联网传输更大量的数据。用户界面必须能够显示正在进行中的传输进度对话框。进度对话框应该不间断地更新(每秒更新5到20次)。用户可以随时关闭进度对话框,如果需要,可以重新调用进度对话框。此外,假装提供参数,如果对话框可见,则必须处理每个进度事件。用户可以在进度对话框上单击取消,并通过修改事件参数,取消操作。



现在我需要一个适合下列限制条件的解决方案:


  1. 允许工作线程调用控件/表单上的方法并阻止/等待,直到执行完成。

  2. 允许对话框本身在初始化时调用相同的方法(因此不使用调用)。

  3. 不会对处理方法或调用造成负担事件,解决方案只应该更改事件订阅本身。

  4. 适当地处理阻止调用可能处于处理过程中的对话框。不幸的是,这不像检查IsDisposed那么容易。

  5. 必须能够与任何事件类型一起使用(假设为EventHandler类型的代理人)

  6. 不能将异常转换为TargetInvocationException。

  7. 解决方案必须与.Net 2.0及更高版本一起使用



<因此,上述限制可以解决这个问题吗?我搜索并挖掘了无数的博客和讨论,唉,我还是空手而复。



更新:我意识到这个问题没有简单的答案。我只在这个网站上呆了好几天,我看过一些有很多经验的人回答问题。我希望其中一个人已经足够解决了这个问题,因为我不会花一周时间来建立一个合理的解决方案。



更新#2 :好的,我会尝试更详细地描述这个问题,看看什么(如果有的话)摇摆出来。以下属性允许我们确定它的状态有一些事情引起关注...


  1. Control.InvokeRequired = Documented如果在当前线程上运行,或者IsHandleCreated为所有父节点返回false,则返回false。
    我受到InvokeRequired实现的困扰,有可能抛出ObjectDisposedException或者甚至可能重新创建对象的句柄。而且由于InvokeRequired可以在我们无法调用(处理进行中)时返回true,即使我们可能需要使用invoke(正在创建),在所有情况下都不能被信任。唯一的情况我可以看到我们可以信任的InvokeRequired返回false是IsHandleCreated在调用之前和之后都返回true(BTW的InvokeRequired的MSDN文档提到检查IsHandleCreated)。


  2. Control.IsHandleCreated =如果句柄已分配给控件,则返回true;否则,假。
    虽然IsHandleCreated是一个安全的调用,但如果控件正在重新创建它的句柄,则可能会崩溃。在访问IsHandleCreated和InvokeRequired时,通过执行锁定(控件)可以解决这个潜在的问题。


  3. Control.Disposing =如果控件在处理过程。


  4. Control.IsDisposed =如果控件已被处理,则返回true。
    我正在考虑订阅Disposed事件并检查IsDisposed属性以确定BeginInvoke是否完成。这里最大的问题是在Dispose - > Disposed转换期间缺少同步锁定。如果您订阅了Disposed事件,那么验证Dispose == false&&& IsDisposed == false,你仍然可能看不到Dispose事件触发。这是因为Dispose的实现设置Dispose = false,然后设置Disposed = true。这为您提供了一个操作性(尽管如此),在处理的控制中读取Dispose和DisDisposed都是false。

...我的头痛:(希望上面的信息会让任何有这些麻烦的人有更多的亮点。我感谢您的备用思考周期。



关闭麻烦...以下是Control.DestroyHandle()方法的后半部分:

  if(!this.RecreatingHandle&&(this.threadCallbackList!= null))
{
lock (this.threadCallbackList)
{
异常异常= new ObjectDisposedException(base.GetType()。Name);
while(this.threadCallbackList.Count> 0)
{
ThreadMethodEntry entry =(ThreadMethodEntry)this.threadCallbackList.Dequeue();
entry.exception = exception;
entry.Complete();
}
}

if((0x40&((int)((long)UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window,this.InternalHandle),-20))))!= 0)
{
UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle,0x10,IntPtr.Zero,IntPtr.Zero);
}
else
{
this.window.DestroyHandle();
}

您会注意到派生到所有等待的跨线程调用的ObjectDisposedException。不久之后,调用this.window.DestroyHandle(),这又会破坏窗口并将其设置为IntPtr.Zero的句柄引用,从而防止进一步调用BeginInvoke方法(或更准确地说是处理BeginInvoke和Invoke的MarshaledInvoke)。这里的问题是,在LockCallbackList上的锁释放之后,可以在控件的线程处于窗口句柄之前插入一个新条目。这似乎是我看到的,尽管很少,通常足以停止发布。



更新#4:



抱歉,继续拖动这个;但是,我认为这里值得记录。我已经设法解决了上述大部分问题,我正在缩小一个可行的解决方案。我再次遇到一个我担心的问题,但到现在为止,还没有看到野外。



这个问题与天才有关写了Control.Handle属性:

  public IntPtr get_Handle()
{
if((checkForIllegalCrossThreadCalls& ;&!inCrossThreadSafeCall)&& this.InvokeRequired)
{
throw new InvalidOperationException(SR.GetString(IllegalCrossThreadCall,new object [] {this.Name}));
}
if(!this.IsHandleCreated)
{
this.CreateHandle();
}
return this.HandleInternal;
}

这本身不是那么糟(不管我对{{修饰);但是,当与InvokeRequired属性或Invoke / BeginInvoke方法组合时,它是坏的。以下是Invoke的基本流程:

  if(!this.IsHandleCreated)
throw;
...做更多的东西
PostMessage(this.Handle,...);

这里的问题是,从另一个线程我可以顺利地通过第一个if语句,之后句柄被控件的线程破坏,从而导致Handle属性的获取在我的线程上重新创建窗口句柄。这可能会导致在原始控件的线程上引发异常。这真的让我失望了,因为没有办法防范这个。如果他们只使用InternalHandle属性并对IntPtr.Zero的结果进行测试,这不会是一个问题。

解决方案

如上所述,整齐地适合 BackgroundWorker - 为什么不只是使用它?您对解决方案的要求太过于通用,相当不合理 - 我怀疑有任何解决方案可以满足他们。


I'm still plagued by background threading in a WinForm UI. Why? Here are some of the issues:

  1. Obviously the most important issue, I can not modify a Control unless I'm executing on the same thread that created it.
  2. As you know, Invoke, BeginInvoke, etc are not available until after a Control is created.
  3. Even after RequiresInvoke returns true, BeginInvoke can still throw ObjectDisposed and even if it doesn't throw, it may never execute the code if the control is being destroyed.
  4. Even after RequiresInvoke returns true, Invoke can indefinitely hang waiting for execution by a control that was disposed at the same time as the call to Invoke.

I'm looking for an elegant solution to this problem, but before I get into specifics of what I'm looking for I thought I would clarify the problem. This is to take the generic problem and put a more concrete example behind it. For this example let's say we are transferring larger amounts of data over the internet. The user interface must be able to show a progress dialog for the transfer already in-progress. The progress dialog should update constantly and quickly (updates 5 to 20 times per second). The user can dismiss the progress dialog at any time and recall it again if desired. And further, lets pretend for arguments sake that if the dialog is visible, it must process every progress event. The user can click Cancel on the progress dialog and via modifying the event args, cancel the operation.

Now I need a solution that will fit in the following box of constraints:

  1. Allow a worker thread to call a method on a Control/Form and block/wait until execution is complete.
  2. Allow the dialog itself to call this same method at initialization or the like (and thus not use invoke).
  3. Place no burden of implementation on the handling method or the calling event, the solution should only change the event subscription itself.
  4. Appropriately handle blocking invokes to a dialog that might be in the process of disposing. Unfortunately this is not as easy as checking for IsDisposed.
  5. Must be able to be used with any event type (assume a delegate of type EventHandler)
  6. Must not translate exceptions to TargetInvocationException.
  7. The solution must work with .Net 2.0 and higher

So, can this be solved given the constraints above? I've searched and dug through countless blogs and discussions and alas I'm still empty handed.

Update: I do realize that this question has no easy answer. I've only been on this site for a couple of days and I've seen some people with a lot of experience answering questions. I'm hoping that one of these individuals has solved this sufficiently enough for me to not spend the week or so it will take to build a reasonable solution.

Update #2: Ok, I'm going to try and describe the problem in a little more detail and see what (if anything) shakes out. The following properties that allow us to determine it's state have a couple of things raise concerns...

  1. Control.InvokeRequired = Documented to return false if running on current thread or if IsHandleCreated returns false for all parents. I'm troubled by the InvokeRequired implementation having the potential to either throw ObjectDisposedException or potentially even re-create the object's handle. And since InvokeRequired can return true when we are not able to invoke (Dispose in progress) and it can return false even though we might need to use invoke (Create in progress) this simply can't be trusted in all cases. The only case I can see where we can trust InvokeRequired returning false is when IsHandleCreated returns true both before and after the call (BTW the MSDN docs for InvokeRequired do mention checking for IsHandleCreated).

  2. Control.IsHandleCreated = Returns true if a handle has been assigned to the control; otherwise, false. Though IsHandleCreated is a safe call it may breakdown if the control is in the process of recreating it's handle. This potential problem appears to be solveable by performing a lock(control) while accessing the IsHandleCreated and InvokeRequired.

  3. Control.Disposing = Returns true if the control is in the process of disposing.

  4. Control.IsDisposed = Returns true if the control has been disposed. I'm considering subscribing to the Disposed event and checking the IsDisposed property to determin if BeginInvoke will ever complete. The big problem here is the lack of a syncronization lock durring the Disposing -> Disposed transition. It's possible that if you subscribe to the Disposed event and after that verify that Disposing == false && IsDisposed == false you still may never see the Disposed event fire. This is due to the fact that the implementation of Dispose sets Disposing = false, and then sets Disposed = true. This provides you an oppertunity (however small) to read both Disposing and IsDisposed as false on a disposed control.

... my head hurts :( Hopefully the information above will shed a little more light on the issues for anyone having these troubles. I appreciate your spare thought cycles on this.

Closing in on the trouble... The following is the later half of the Control.DestroyHandle() method:

if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
    lock (this.threadCallbackList)
    {
        Exception exception = new ObjectDisposedException(base.GetType().Name);
        while (this.threadCallbackList.Count > 0)
        {
            ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
            entry.exception = exception;
            entry.Complete();
        }
    }
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
    UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
    this.window.DestroyHandle();
}

You'll notice the ObjectDisposedException being dispatched to all waiting cross-thread invocations. Shortly following this is the call to this.window.DestroyHandle() which in turn destroys the window and set's it's handle reference to IntPtr.Zero thereby preventing further calls into the BeginInvoke method (or more precisely MarshaledInvoke which handle both BeginInvoke and Invoke). The problem here is that after the lock releases on threadCallbackList a new entry can be inserted before the Control's thread zeros the window handle. This appears to be the case I'm seeing, though infrequently, often enough to stop a release.

Update #4:

Sorry to keep dragging this on; however, I thought it worth documenting here. I've managed to solve most of the problems above and I'm narrowing in on a solution that works. I've hit one more issue I was concerned about, but until now, have not seen 'in-the-wild'.

This issue has to do with the genius that wrote Control.Handle property:

    public IntPtr get_Handle()
    {
        if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
        {
            throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
        }
        if (!this.IsHandleCreated)
        {
            this.CreateHandle();
        }
        return this.HandleInternal;
    }

This by itself is not so bad (regardless of my opinions on get { } modifications); however, when combined with the InvokeRequired property or the Invoke/BeginInvoke method it is bad. Here is the basic flow the Invoke:

if( !this.IsHandleCreated )
    throw;
... do more stuff
PostMessage( this.Handle, ... );

The issue here is that from another thread I can successfully pass through the first if statement, after which the handle is destroyed by the control's thread, thus causing the get of the Handle property to re-create the window handle on my thread. This then can cause an exception to be raised on the original control's thread. This one really has me stumped as there is no way to guard against this. Had they only use the InternalHandle property and tested for result of IntPtr.Zero this would not be an issue.

解决方案

Your scenario, as described, neatly fits BackgroundWorker - why not just use that? Your requirements for a solution are way too generic, and rather unreasonable - I doubt there is any solution that would satisfy them all.

这篇关于在跨线程WinForm事件处理中避免Invoke / BeginInvoke的困扰?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆