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

csh*_*net 48 .net c# events multithreading winforms

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

  1. 显然是最重要的问题,除非我在创建它的同一个线程上执行,否则我无法修改控件.
  2. 如您所知,Invoke,BeginInvoke等在创建Control之后才可用.
  3. 即使在RequiresInvoke返回true之后,BeginInvoke仍然可以抛出ObjectDisposed,即使它没有抛出,如果控件被销毁,它也可能永远不会执行代码.
  4. 即使在RequiresInvoke返回true之后,Invoke也可以无限期地挂起等待由调用Invoke同时处理的控件执行.

我正在寻找这个问题的优雅解决方案,但在我了解我正在寻找的具体内容之前,我想我会澄清问题.这是为了解决一般性问题并在其背后提出一个更具体的例子.对于这个例子,假设我们正在通过互联网传输大量数据.用户界面必须能够显示正在进行的传输的进度对话框.进度对话框应该持续快速更新(每秒更新5到20次).用户可以随时关闭进度对话框,并在需要时再次调用它.而且,让我们假装参数,如果对话框可见,它必须处理每个进度事件.用户可以单击进度对话框上的取消,并通过修改事件参数,取消操作.

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

  1. 允许工作线程调用Control/Form上的方法并阻塞/等待,直到执行完成.
  2. 允许对话框本身在初始化等时调用相同的方法(因此不使用invoke).
  3. 对处理方法或调用事件不施加任何实施负担,解决方案应仅更改事件订阅本身.
  4. 适当地处理阻塞调用到可能正在处理的对话框.不幸的是,这并不像检查IsDisposed那么容易.
  5. 必须能够与任何事件类型一起使用(假设类型为EventHandler的委托)
  6. 不得将异常转换为TargetInvocationException.
  7. 该解决方案必须与.Net 2.0及更高版本配合使用

那么,鉴于上述限制,这可以解决吗?我搜索并挖掘了无数的博客和讨论,唉,我还是空手而归.

更新:我确实意识到这个问题没有简单的答案.我只在这个网站上呆了几天,我见过一些有很多回答问题经验的人.我希望这些人中的一个能够充分解决这个问题,这样我就不会花费一周时间来建立一个合理的解决方案.

更新#2:好的,我将尝试更详细地描述问题,看看有什么(如果有的话)震动.允许我们确定其状态的以下属性引起了一些问题...

  1. Control.InvokeRequired =记录如果在当前线程上运行或者IsHandleCreated为所有父项返回false,则返回false.我很担心InvokeRequired实现有可能抛出ObjectDisposedException或甚至可能重新创建对象的句柄.并且由于InvokeRequired在我们无法调用(正在进行Dispose)时可以返回true,并且它可以返回false,即使我们可能需要使用invoke(正在创建),这在所有情况下都不可信任.唯一可以看到我们可以信任的地方InvokeRequired返回false是当IsHandleCreated在调用之前和之后都返回true时(BTW,InvokeRequired的MSDN文档确实提到了对IsHandleCreated的检查).

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

  3. Control.Disposing =如果控件处于处理过程中,则返回true.

  4. Control.IsDisposed =如果控件已被释放,则返回true.我正在考虑订阅Disposed事件并检查IsDisposed属性以确定BeginInvoke是否会完成.这里的一个大问题是在Disposing - > Disposed过渡期间缺少同步锁定.如果您订阅Disposed事件并在此之后验证Disposing == false && IsDisposed == false,您可能永远不会看到Disposed事件触发.这是因为Dispose的实现设置Disposing = false,然后设置Disposed = true.这为您提供了一个机会(无论多小),以便在已处理的控件上将Disposing和IsDisposed都读为false.

......我的头疼了:(希望上面的信息可以为那些遇到这些麻烦的人提供更多的解决方案.我很感激你的思考周期.

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

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();
}
Run Code Online (Sandbox Code Playgroud)

您会注意到ObjectDisposedException被调度到所有等待的跨线程调用.紧接着是对this.window.DestroyHandle()的调用,它反过来破坏窗口并设置它对IntPtr.Zero的句柄引用,从而防止进一步调用BeginInvoke方法(或更准确地说是MarshaledInvoke,它同时处理BeginInvoke和Invoke).这里的问题是在threadCallbackList上发布锁之后,可以在Control的线程将窗口句柄归零之前插入一个新条目.这似乎是我看到的情况,虽然很少,但通常足以阻止释放.

更新#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;
    }
Run Code Online (Sandbox Code Playgroud)

这本身并不是那么糟糕(无论我对get {}修改的意见如何); 但是,当与InvokeRequired属性或Invoke/BeginInvoke方法结合使用时,它很糟糕.这是Invoke的基本流程:

if( !this.IsHandleCreated )
    throw;
... do more stuff
PostMessage( this.Handle, ... );
Run Code Online (Sandbox Code Playgroud)

这里的问题是,从另一个线程我可以成功地传递第一个if语句,之后句柄被控件的线程销毁,从而导致Handle属性的get在我的线程上重新创建窗口句柄.这可能会导致在原始控件的线程上引发异常.这个真的让我难过,因为没有办法防范这一点.如果他们只使用InternalHandle属性并测试IntPtr.Zero的结果,这不会是一个问题.

Pav*_*aev 22

如上所述,您的方案非常适合BackgroundWorker- 为什么不使用它?您对解决方案的要求过于笼统,而且不合理 - 我怀疑是否有任何解决方案可以满足所有这些要求.


Jar*_*Par 8

我在一段时间后遇到了这个问题,并提出了涉及同步上下文的解决方案.解决方案是向SynchronizationContext添加扩展方法,该方法将特定委托绑定到SynchronizationContext绑定的线程.它将生成一个新的委托,在调用时将封送对appropraite线程的调用,然后调用原始委托.它使代表的消费者几乎不可能在错误的上下文中调用它.

关于这个主题的博客文章:


csh*_*net 7

好的,几天后我已经完成了一个解决方案.它解决了初始帖子中列出的所有约束和目标.用法简单明了:

myWorker.SomeEvent += new EventHandlerForControl<EventArgs>(this, myWorker_SomeEvent).EventHandler;
Run Code Online (Sandbox Code Playgroud)

当工作线程调用此事件时,它将处理对控制线程的所需调用.它确保它不会无限期挂起,如果无法在控制线程上执行,它将始终抛出ObjectDisposedException.我已经创建了该类的其他派生,一个用于忽略错误,另一个用于在控件不可用时直接调用委托.似乎运作良好,并完全通过了几个重现上述问题的测试.在不违反上述约束#3的情况下,我无法阻止解决方案只有一个问题.这个问题是问题描述中的最后一个(Update#4),get Handle中的线程问题.这可能会导致原始控制线程出现意外行为,并且我经常看到在调用Dispose()时抛出的InvalidOperationException(),因为在我的线程上创建过程中的句柄.为了允许处理这个问题,我确保锁定访问将使用Control.Handle属性的函数.这允许表单在调用基本实现之前重载DestroyHandle方法并锁定.如果这样做,这个类应该完全是线程安全的(据我所知).

public class Form : System.Windows.Forms.Form
{
    protected override void DestroyHandle()
    {
        lock (this) base.DestroyHandle();
    }
}
Run Code Online (Sandbox Code Playgroud)

你可能会注意到解决死锁的核心方面变成了一个轮询循环.最初我通过处理Disposed和HandleDestroyed的控件事件并使用多个等待句柄成功解决了测试用例.经过仔细审查后,我发现这些事件的订阅/取消订阅不是线程安全的.因此,我选择轮询IsHandleCreated,以免在线程的事件上产生不必要的争用,从而避免仍然产生死锁状态的可能性.

无论如何,这是我提出的解决方案:

/// <summary>
/// Provies a wrapper type around event handlers for a control that are safe to be
/// used from events on another thread.  If the control is not valid at the time the
/// delegate is called an exception of type ObjectDisposedExcpetion will be raised.
/// </summary>
[System.Diagnostics.DebuggerNonUserCode]
public class EventHandlerForControl<TEventArgs> where TEventArgs : EventArgs
{
    /// <summary> The control who's thread we will use for the invoke </summary>
    protected readonly Control _control;
    /// <summary> The delegate to invoke on the control </summary>
    protected readonly EventHandler<TEventArgs> _delegate;

    /// <summary>
    /// Constructs an EventHandler for the specified method on the given control instance.
    /// </summary>
    public EventHandlerForControl(Control control, EventHandler<TEventArgs> handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");
        _delegate = handler;
    }

    /// <summary>
    /// Constructs an EventHandler for the specified delegate converting it to the expected
    /// EventHandler&lt;TEventArgs> delegate type.
    /// </summary>
    public EventHandlerForControl(Control control, Delegate handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");

        //_delegate = handler.Convert<EventHandler<TEventArgs>>();
        _delegate = handler as EventHandler<TEventArgs>;
        if (_delegate == null)
        {
            foreach (Delegate d in handler.GetInvocationList())
            {
                _delegate = (EventHandler<TEventArgs>) Delegate.Combine(_delegate,
                    Delegate.CreateDelegate(typeof(EventHandler<TEventArgs>), d.Target, d.Method, true)
                );
            }
        }
        if (_delegate == null) throw new ArgumentNullException("_delegate");
    }


    /// <summary>
    /// Used to handle the condition that a control's handle is not currently available.  This
    /// can either be before construction or after being disposed.
    /// </summary>
    protected virtual void OnControlDisposed(object sender, TEventArgs args)
    {
        throw new ObjectDisposedException(_control.GetType().Name);
    }

    /// <summary>
    /// This object will allow an implicit cast to the EventHandler&lt;T> type for easier use.
    /// </summary>
    public static implicit operator EventHandler<TEventArgs>(EventHandlerForControl<TEventArgs> instance)
    { return instance.EventHandler; }

    /// <summary>
    /// Handles the 'magic' of safely invoking the delegate on the control without producing
    /// a dead-lock.
    /// </summary>
    public void EventHandler(object sender, TEventArgs args)
    {
        bool requiresInvoke = false, hasHandle = false;
        try
        {
            lock (_control) // locked to avoid conflicts with RecreateHandle and DestroyHandle
            {
                if (true == (hasHandle = _control.IsHandleCreated))
                {
                    requiresInvoke = _control.InvokeRequired;
                    // must remain true for InvokeRequired to be dependable
                    hasHandle &= _control.IsHandleCreated;
                }
            }
        }
        catch (ObjectDisposedException)
        {
            requiresInvoke = hasHandle = false;
        }

        if (!requiresInvoke && hasHandle) // control is from the current thread
        {
            _delegate(sender, args);
            return;
        }
        else if (hasHandle) // control invoke *might* work
        {
            MethodInvokerImpl invocation = new MethodInvokerImpl(_delegate, sender, args);
            IAsyncResult result = null;
            try
            {
                lock (_control)// locked to avoid conflicts with RecreateHandle and DestroyHandle
                    result = _control.BeginInvoke(invocation.Invoker);
            }
            catch (InvalidOperationException)
            { }

            try
            {
                if (result != null)
                {
                    WaitHandle handle = result.AsyncWaitHandle;
                    TimeSpan interval = TimeSpan.FromSeconds(1);
                    bool complete = false;

                    while (!complete && (invocation.MethodRunning || _control.IsHandleCreated))
                    {
                        if (invocation.MethodRunning)
                            complete = handle.WaitOne();//no need to continue polling once running
                        else
                            complete = handle.WaitOne(interval);
                    }

                    if (complete)
                    {
                        _control.EndInvoke(result);
                        return;
                    }
                }
            }
            catch (ObjectDisposedException ode)
            {
                if (ode.ObjectName != _control.GetType().Name)
                    throw;// *likely* from some other source...
            }
        }

        OnControlDisposed(sender, args);
    }

    /// <summary>
    /// The class is used to take advantage of a special-case in the Control.InvokeMarshaledCallbackDo()
    /// implementation that allows us to preserve the exception types that are thrown rather than doing
    /// a delegate.DynamicInvoke();
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCode]
    private class MethodInvokerImpl
    {
        readonly EventHandler<TEventArgs> _handler;
        readonly object _sender;
        readonly TEventArgs _args;
        private bool _received;

        public MethodInvokerImpl(EventHandler<TEventArgs> handler, object sender, TEventArgs args)
        {
            _received = false;
            _handler = handler;
            _sender = sender;
            _args = args;
        }

        public MethodInvoker Invoker { get { return this.Invoke; } }
        private void Invoke() { _received = true; _handler(_sender, _args); }

        public bool MethodRunning { get { return _received; } }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你在这里看到任何错误,请告诉我.