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

时间:2009-09-01 19:08:16

标签: c# .net winforms multithreading events

13 个答案:

答案 0 :(得分:22)

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

答案 1 :(得分:8)

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

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

答案 2 :(得分:7)

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

myWorker.SomeEvent += new EventHandlerForControl<EventArgs>(this, myWorker_SomeEvent).EventHandler;

当工作线程调用此事件时,它将处理对控制线程的所需调用。它确保它不会无限期挂起,如果无法在控制线程上执行,它将始终抛出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();
    }
}

您可能会注意到解决死锁的核心方面成为了一个轮询循环。最初我通过处理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; } }
    }
}

如果您在此处发现任何错误,请告知我们。

答案 3 :(得分:2)

我不会为您提供满足您所有要求的详尽解决方案,但我会提供观点。但总的来说,我认为你是按照这些要求拍摄月球的。

Invoke / BeginInvoke体系结构只是通过向控件的UI线程发送一条Windows消息来执行提供的委托,并且消息循环本身执行委托。这一点的具体工作方式是无关紧要的,但重点是没有特别的原因,您必须使用此架构与UI线程进行线程同步。您所需要的只是运行其他一些循环,例如在Forms.Timer或类似的循环中,监视Queue以便委托执行并执行此操作。实现自己的实现起来相当简单,但我不知道InvokeBeginInvoke没有为您提供具体的内容。

答案 4 :(得分:1)

这不是问题第二部分的真正答案,但我会将其仅作参考:

private delegate object SafeInvokeCallback(Control control, Delegate method, params object[] parameters);
public static object SafeInvoke(this Control control, Delegate method, params object[] parameters)
{
    if (control == null)
        throw new ArgumentNullException("control");
    if (control.InvokeRequired)
    {
        IAsyncResult result = null;
        try { result = control.BeginInvoke(new SafeInvokeCallback(SafeInvoke), control, method, parameters); }
        catch (InvalidOperationException) { /* This control has not been created or was already (more likely) closed. */ }
        if (result != null)
            return control.EndInvoke(result);
    }
    else
    {
        if (!control.IsDisposed)
            return method.DynamicInvoke(parameters);
    }
    return null;
}

此代码应避免使用Invoke / BeginInvoke最常见的陷阱,并且易于使用。转身

if (control.InvokeRequired)
    control.Invoke(...)
else
    ...

control.SafeInvoke(...)

BeginInvoke可以使用类似的结构。

答案 5 :(得分:1)

哇,长问题。 我会尝试整理我的答案,这样你就可以纠正我,如果我理解错了,好吗?

1)除非你有充分的理由直接从不同的线程调用UI方法,否则不要。您始终可以使用事件处理程序转到生产者/消费者模型:

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}
例如,每当不同线程中的组件需要在UI中执行某些操作时,

myHandler将被触发。此外,在OnLoad中设置事件处理程序并在OnClosing中取消订阅可确保在创建句柄并准备好处理事件时,UI仅接收/处理事件。如果处理过程中你甚至无法向该对话框发出事件,因为你将不再订阅该事件。如果在处理其中一个事件时触发另一个事件,它将排队。

您可以在事件参数中传递所需的所有信息:您是在更新进度,关闭窗口等等。

2)如果您使用我上面建议的模型,则不需要InvokeRequired。在这个例子中,你知道触发myHandler的唯一事情就是你的组件,例如,它存在于另一个线程中。

private void myHandler(object sender, EventArgs args)
{
    BeginInvoke(Action(myMethod));
}

因此,您始终可以使用调用来确保您处于正确的线程中。

3)注意同步呼叫。如果需要,可以替换使用Invoke而不是BeginInvoke。这将阻止您的组件,直到事件处理完毕。但是,如果在UI中您需要与组件所在线程所独有的内容进行通信,则可能会出现死锁问题。 (我不知道自己是否清楚,请告诉我)。我在使用反射(TargetInvocationException)和BeginInvoke时遇到异常问题(因为它们启动了不同的线程,你丢失了部分堆栈跟踪),但我不记得Invoke调用有很多麻烦,所以你应该在涉及例外情况时要安全。

哇,哇,答案很长。如果有任何机会我错过了你的任何要求或误解了你说的话(英语不是我的母语,所以我们永远不确定),请告诉我。

答案 6 :(得分:1)

我尝试将所有这样的调用消息组织到GUI中作为fire和forget(处理由于处理表单时的竞争条件而导致GUI可以抛出的异常)。

这种方式如果它永远不会执行,则不会造成伤害。

如果GUI需要响应工作线程,它有一种有效反转通知的方法。对于简单的需求,BackgroundWorker已经处理了这个问题。

答案 7 :(得分:1)

这是一个非常困难的问题。正如我在评论中提到的,考虑到记录的约束,我认为它不可解决。你可以通过.net框架的特定实现来破解它:知道各种成员函数的实现可以通过在这里和那里抓住锁来帮助你作弊,并且知道“它实际上是好的,在不同的线程上调用其他成员函数。 “

所以,我现在的基本答案是,“不。”我不愿意说这是不可能的,因为我对.Net框架有很大的信心。另外,我比较新手,没有研究过一般的框架,或CS,但互联网是开放的(甚至对像我这样无知的人)!

在另一个主题上,论证可以提出并得到很好的支持,“你永远不需要Invoke,只需要使用BeginInvoke,然后开火并忘记。”我不打算试图支持它,或者甚至说它是一个正确的断言,但我会说常见的实现是不正确的,并且构成一个工作(我希望)。

这是一个常见的实现(取自这里的不同答案):

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

这不是线程安全的。组件可以在取消订阅之前很容易地开始调用调用列表,并且只有在我们完成处理之后才会调用处理程序。真正的问题是,没有记录每个组件必须如何使用.Net中的事件机制,老实说,他根本不需要取消订阅:一旦你发出你的电话号码,没有人需要删除它!

更好的是:

protected override void OnLoad(System.EventArgs e)
{
    component.Event += new System.EventHandler(myHandler);
}    
protected override void OnFormClosing(FormClosedEventArgs e)
{
    component.Event -= new System.EventHandler(myHandler);
    lock (lockobj)
    {
        closing = true;
    }
}
private void Handler(object a, System.EventArgs e)
{
    lock (lockobj)
    {
        if (closing)
            return;
        this.BeginInvoke(new System.Action(HandlerImpl));
    }
}
/*Must be called only on GUI thread*/
private void HandlerImpl()
{
    this.Hide();
}
private readonly object lockobj = new object();
private volatile bool closing = false;

如果我错过了什么,请告诉我。

答案 8 :(得分:0)

如果您不喜欢BackgroundWoker(如@Pavel所述),您可能需要查看此库http://www.wintellect.com/PowerThreading.aspx

答案 9 :(得分:0)

如果我理解这一点,为什么在应用程序运行时需要处理进度对话框?为什么不在用户请求中显示和隐藏它?这听起来会让你的问题至少更简单一些。

答案 10 :(得分:0)

为什么不在用户解雇时隐藏对话框?如果你没有以模态方式显示该对话框,那应该可以正常工作。 (使用show而不是showdialog)。我相信你可以通过在调用show时将主机传递给对话框,将你的进度对话框放在你自己的窗口之上(如果你需要)。

答案 11 :(得分:0)

创建System.ComponentModel.ISynchronizeInvoke时使用System.ComponentModel.Component非常有用,例如BackgroundWorker。以下代码段是FileSystemWater处理事件的方式。

    ''' <summary>
    ''' Gets or sets the object used to marshal the event handler calls issued as a result of finding a file in a search.
    ''' </summary>
    <IODescription(SR.FSS_SynchronizingObject), DefaultValue(CType(Nothing, String))> _
    Public Property SynchronizingObject() As System.ComponentModel.ISynchronizeInvoke
        Get
            If (_synchronizingObject Is Nothing) AndAlso (MyBase.DesignMode) Then
                Dim oHost As IDesignerHost = DirectCast(MyBase.GetService(GetType(IDesignerHost)), IDesignerHost)
                If (Not (oHost Is Nothing)) Then
                    Dim oRootComponent As Object = oHost.RootComponent
                    If (Not (oRootComponent Is Nothing)) AndAlso (TypeOf oRootComponent Is ISynchronizeInvoke) Then
                        _synchronizingObject = DirectCast(oRootComponent, ISynchronizeInvoke)
                    End If
                End If
            End If
            Return _synchronizingObject
        End Get
        Set(ByVal Value As System.ComponentModel.ISynchronizeInvoke)
            _synchronizingObject = Value
        End Set
    End Property

    Private _onStartupHandler As EventHandler

    Protected Sub OnStartup(ByVal e As EventArgs)
        If ((Not Me.SynchronizingObject Is Nothing) AndAlso Me.SynchronizingObject.InvokeRequired) Then
            Me.SynchronizingObject.BeginInvoke(_onStartupHandler, New Object() {Me, e})
        Else
            _onStartupHandler.Invoke(Me, e)
        End If
    End Sub

答案 12 :(得分:0)

这是我目前正在使用的内容。它基于SynchronizationContext的使用,并受到JaredPar博客文章的启发 - 请参阅上面的回答。这可能不完美,但确实避免了我遇到的一些OP问题。

   // Homemade Action-style delegates to provide .Net 2.0 compatibility, since .Net 2.0 does not 
   //  include a non-generic Action delegate nor Action delegates with more than one generic type 
   //  parameter. (The DMethodWithOneParameter<T> definition is not needed, could be Action<T> 
   //  instead, but is defined for consistency.) Some interesting observations can be found here:
   //  http://geekswithblogs.net/BlackRabbitCoder/archive/2011/11/03/c.net-little-wonders-the-generic-action-delegates.aspx
   public delegate void DMethodWithNoParameters();
   public delegate void DMethodWithOneParameter<T>(T parameter1);
   public delegate void DMethodWithTwoParameters<T1, T2>(T1 parameter1, T2 parameter2);
   public delegate void DMethodWithThreeParameters<T1, T2, T3>(T1 parameter1, T2 parameter2, T3 parameter3);


   /// <summary>
   /// Class containing support code to use the SynchronizationContext mechanism to dispatch the 
   /// execution of a method to the WinForms UI thread, from another thread. This can be used as an 
   /// alternative to the Control.BeginInvoke() mechanism which can be problematic under certain 
   /// conditions. See for example the discussion here:
   /// http://stackoverflow.com/questions/1364116/avoiding-the-woes-of-invoke-begininvoke-in-cross-thread-winform-event-handling
   ///
   /// As currently coded this works with methods that take zero, one, two or three arguments, but 
   /// it is a trivial job to extend the code for methods taking more arguments.
   /// </summary>
   public class WinFormsHelper
   {
      // An arbitrary WinForms control associated with thread 1, used to check that thread-switching 
      //  with the SynchronizationContext mechanism should be OK
      private readonly Control _thread1Control = null;

      // SynchronizationContext for the WinForms environment's UI thread
      private readonly WindowsFormsSynchronizationContext _synchronizationContext;


      /// <summary>
      /// Constructor. This must be called on the WinForms UI thread, typically thread 1. (Unless 
      /// running under the Visual Studio debugger, then the thread number is arbitrary.)
      ///
      /// The provided "thread 1 control" must be some WinForms control that will remain in 
      /// existence for as long as this object is going to be used, for example the main Form 
      /// control for the application.
      /// </summary>
      /// <param name="thread1Control">see above</param>
      public WinFormsHelper(Control thread1Control)
      {
         _thread1Control = thread1Control;
         if (thread1Control.InvokeRequired)
            throw new Exception("Not called on thread associated with WinForms controls.");

         _synchronizationContext =
                            SynchronizationContext.Current as WindowsFormsSynchronizationContext;
         if (_synchronizationContext == null) // Should not be possible?
            throw new Exception("SynchronizationContext.Current = null or wrong type.");
      }


      // The following BeginInvoke() methods follow a boilerplate pattern for how these methods 
      // should be implemented - they differ only in the number of arguments that the caller wants 
      // to provide.

      public void BeginInvoke(DMethodWithNoParameters methodWithNoParameters)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithNoParameters();
         }, null);
      }


      public void BeginInvoke<T>(DMethodWithOneParameter<T> methodWithOneParameter, T parameter1)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithOneParameter(parameter1);
         }, null);
      }


      public void BeginInvoke<T1, T2>(DMethodWithTwoParameters<T1, T2> methodWithTwoParameters,
                                      T1 parameter1, T2 parameter2)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithTwoParameters(parameter1, parameter2);
         }, null);
      }


      public void BeginInvoke<T1, T2, T3>(DMethodWithThreeParameters<T1, T2, T3> methodWithThreeParameters,
                                          T1 parameter1, T2 parameter2, T3 parameter3)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithThreeParameters(parameter1, parameter2, parameter3);
         }, null);
      }
   }