避免在跨线程WinForm事件处理中使用Invoke/BeginInvoke带来的问题。

49

在WinForm UI中,我仍然受到后台线程的困扰。为什么呢?下面是一些问题:

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

我正在寻找一个优雅的解决方案来解决这个问题,但在进入我需要的具体内容之前,我想澄清这个问题。这是为了将通用问题具体化。假设我们正在通过互联网传输大量数据,用户界面必须能够显示当前正在进行的传输的进度对话框。进度对话框应该快速而持续地更新(每秒更新5到20次)。用户可以随时取消进度对话框,如果需要,可以再次调用它。此外,让我们假装,如果对话框可见,它必须处理每个进度事件。用户可以在进度对话框上单击“取消”,通过修改事件参数,取消操作。

现在我需要一个符合以下约束条件的解决方案:

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

所以,考虑到上述限制条件,这个问题是否可以解决?我已经搜索和研究了无数的博客和讨论,但我仍然一筹莫展。

更新:我意识到这个问题没有简单的答案。我在这个网站上只呆了几天,看到了一些有经验的人回答问题。我希望其中有一个人已经解决了这个问题,使我不必花费一周左右的时间来构建一个合理的解决方案。

更新#2:好的,我将尝试更详细地描述问题,并看看会出现什么(如果有)...

  1. Control.InvokeRequired = 返回false,如果在当前线程上运行或如果IsHandleCreated对所有父级都返回false。

  2. Control.IsHandleCreated = 如果已为控件分配了句柄,则返回true;否则返回false。

  3. Control.Disposing = 如果正在处理disposing过程中,则返回true。

  4. Control.IsDisposed = 如果控件已被处理,则返回true。

InvokeRequired的实现可能会引发ObjectDisposedException,甚至可能重新创建对象的句柄。当Dispose正在进行时,InvokeRequired可以返回true,即使需要使用invoke也可能返回false(Create in progress)。因此,在某些情况下,InvokeRequired不能完全信任。唯一可以信任InvokeRequired返回false的情况是,IsHandleCreated在调用之前和之后都返回true(顺便说一下,MSDN文档提到了检查IsHandleCreated)。

虽然IsHandleCreated是一个安全的调用,但如果控件正在重新创建句柄,则可能会出现问题。通过在访问IsHandleCreated和InvokeRequired时执行lock(control),可以解决这个潜在的问题。

我考虑订阅Disposed事件并检查IsDisposed属性,以确定BeginInvoke是否会完成。这里的大问题是,在Disposing -> Disposed转换期间缺乏同步锁。如果您订阅了Disposed事件并验证Disposing == false&& IsDisposed == false,您仍然可能永远不会看到Disposed事件的触发。这是因为Dispose的实现设置Disposing = false,然后设置Disposed = true。这提供了一个机会(尽管很小),让您在处理过的控件上将Disposing和IsDisposed都读取为false。

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

您会注意到ObjectDisposedException被分派到所有等待的跨线程调用。 紧随其后的是对this.window.DestroyHandle()的调用,它销毁窗口并将其句柄引用设置为IntPtr.Zero,从而防止进一步调用BeginInvoke方法(或更准确地说是处理BeginInvoke和Invoke的MarshaledInvoke)。 这里的问题是,在线程CallbackList上释放锁定后,控件的线程可以在将窗口句柄置零之前插入新条目。 尽管不经常发生,但这似乎是我看到的情况,足以阻止发布。

更新#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;
    }

这本身并不是很糟糕(不管我的get { }修改意见如何);然而,当与InvokeRequired属性或Invoke/BeginInvoke方法相结合时就变得糟糕了。以下是Invoke的基本流程:

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

这里的问题在于,另一个线程可以成功通过第一个 if 语句,之后控制线程将销毁句柄,因此会导致在我的线程上重新创建窗口句柄时获取句柄属性。这可能会导致在原始控件线程上引发异常。这真的让我感到困惑,因为没有办法防止这种情况发生。如果他们只使用了 InternalHandle 属性并测试了 IntPtr.Zero 的结果,那么这就不会成为问题。


在提问时,你可以更加礼貌一些。 - H H
13个回答

22

根据你描述的场景,BackgroundWorker 似乎是一个很好的选择,为什么不直接使用它呢?你对解决方案的要求过于泛泛而谈,有些不切实际,我怀疑没有任何一个解决方案能满足你的所有要求。


我同意你的观点,这是个难题。我相信很多人都曾经面临过这个问题。我提出这个问题的原因是我认为这个问题是没有解决方案的;但是,我希望有人可以证明我错了。 - csharptest.net
Pavel,谢谢你带领我了解BackgroundWorker,我之前不知道它的存在。它很适合我描述的场景,我一定会用得上它。 - csharptest.net
这并不是很优雅,但我认为你会欣赏我发布的解决方案,它可以在不受Invoke、BeginInvoke和BackgroundWorker产生的线程问题困扰的情况下正常工作。只要你能确保表单在完成之前不关闭,并且表单不重新创建它的句柄,BackgroundWorker仍然可以很好地完成工作。 - csharptest.net
在搜索博客、新闻组和其他“垃圾”之前,请先搜索对象浏览器。我听说.NET Framework中有超过70,000个对象可供选择。如果您找不到所需的内容,您将会发现一些项目可以帮助您更好地搜索其他“垃圾”。 - AMissico

9
我之前遇到过这个问题,找到了一个涉及同步上下文的解决方案。这个解决方案是在同步上下文中添加一个扩展方法,将特定的委托绑定到同步上下文所绑定的线程。它会生成一个新的委托,当被调用时,会将调用传输到适当的线程,然后调用原始委托。这使得委托的使用者几乎不可能在错误的上下文中调用它。
关于此主题的博客文章:

一个不错的解决方案,稍加努力甚至可以避免DynamicInvoke调用,从而不会生成TargetInvocationException。总的来说,我喜欢这种方法,但我有一个问题:生成方法的开销是多少?当它们不再使用时,它们会被卸载吗?此外,我仍然有点困惑,为什么你选择了方法生成方法,而不是简单地包装委托的类。 - csharptest.net
非常有趣的博客文章。只有一个小问题需要指出:您提到了“ISynchronizedInvoke”五次。我想您是指ISynchronizeInvoke? - RenniePet
你写道,“动态创建委托实例并不是一件简单的事情。除非我们将所有委托签名的排列组合编码到一个类中,否则我们无法使用Delegate.Create API,因为我们无法提供具有匹配签名的方法。”难道这个问题不能通过使用通用的Action<>构造来大大减少吗?然后,您只需要为接受0个参数的方法、接受1个参数的方法、接受2个参数的方法提供支持,这可能已经足够了。或者我完全误解了什么? - RenniePet

7

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

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

当工作线程调用此事件时,它将处理对控制线程的必要调用。 它确保不会无限期挂起,并且如果无法在控制线程上执行,则始终会抛出ObjectDisposedException。 我已创建了该类的其他派生类之一,以忽略错误,另一个则直接调用委托(如果控件不可用)。 看起来效果很好,并且完全通过了重现上述问题的几个测试。 解决方案中只有一个问题,我无法避免违反上述第3个约束条件。 该问题是问题描述中的最后一个(更新#4),即get Handle中的线程问题。 这可能会导致原始控制线程上的意外行为,我经常看到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; } }
    }
}

如果您在这里发现任何错误,请告诉我。

有没有更好的解决方案?我会发一些奖金并观察情况。 - csharptest.net
如果你还感兴趣,请看这个链接:http://stackoverflow.com/questions/4190299/exploiting-the-backgroundworker-for-cross-thread-invocation-of-gui-actions-on-win - Ohad Schneider
在阅读了所有的内容之后,您仍然错过了 MSDN 中最重要的一篇文章:“除了 InvokeRequired 属性之外,控件上还有四个线程安全的方法:InvokeBeginInvokeEndInvokeCreateGraphics(如果控件的句柄已经被创建)。在后台线程调用 CreateGraphics 之前,如果控件的句柄尚未被创建,则可能会导致非法的跨线程调用。” - lmat - Reinstate Monica
换句话说,您不能从不同的线程调用IsHandleCreatedGetType。非常出色的工作,调查这个永恒的问题。我认为它是无法解决的。该模型存在问题,这些问题无法通过文档中记录的限制来克服。 - lmat - Reinstate Monica
你说,“为了避免与ReCreateHandle冲突”。这样做如何避免与ReCreateHandle冲突?我猜你是假设ReCreateHandlethis上加锁了?这个有文档记录吗? - lmat - Reinstate Monica

2
我不会为您编写一个满足所有要求的详尽解决方案,但我会提供一些观点。总的来说,我认为您对这些要求过于苛刻了。 Invoke/BeginInvoke架构仅通过发送Windows消息并由消息循环本身执行委托,从而在控件的UI线程上执行提供的委托。具体的工作原理并不重要,但是要点是,没有特定的理由需要使用这种架构来与UI线程同步线程。您只需要运行其他循环,例如在Forms.Timer中或类似的东西中,监视要执行的委托的Queue,并执行它们。虽然实现自己的循环非常简单,但我不知道它能为您提供什么特别的功能,这些InvokeBeginInvoke不能提供。

我完全同意,这是/一直是我大多数事情的首选(一个简单的生产者/消费者队列)。 我对这个解决方案的问题有两个。 A)它需要对客户端代码(在winform中运行的代码)产生相当大的影响; 和B)它不容易允许阻塞/双向事件。 当然,你可以这样做,创建一个等待句柄,等待消费者处理消息,然后继续; 但是,这会让你回到最初的状态... 处理用户可能处置UI的潜力。 - csharptest.net
在某个层面上,你会被留下来,不管好坏。正如其他人所建议的那样,当用户关闭进度对话框时,实际上并不需要将其丢弃。 - Adam Robinson

1
这是一个相当困难的问题。正如我在评论中提到的,我认为在给定的约束条件下它是无法解决的。你可以通过了解 .net 框架的特定实现来进行黑客攻击:知道各种成员函数的实现可能会帮助你在这里和那里获取锁,并知道“在不同的线程上调用其他成员函数实际上是可以的”。
所以,我的基本答案现在是“不行”。我不想说这是不可能的,因为我对 .Net 框架有很大的信心。此外,我相对来说是个新手,没有研究过框架或计算机科学,但互联网是开放的(即使对于像我这样的无知之人也是如此)!
在另一个话题上,可以提出并得到良好支持的论点是,“你永远不应该需要使用 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;

如果我有遗漏,请告诉我。


这个问题在一段时间前就已经为我解决了,http://csharptest.net/browse/src/Library/Delegates/EventHandlerForControl.cs你需要做的就是在调用基类之前,在DestroyHandle()方法中加上lock(this),然后按照我在这篇帖子中的回答所示,改变你订阅事件的方式。 - csharptest.net
@csharptest.net 你怎么知道这个有效?有文档记录吗? - lmat - Reinstate Monica
阅读BCL代码,分析问题并实现解决方案需要一些时间。经过多年的生产和一系列严格的自动化测试后,我对其行为非常有信心。如果您使用ForcedEventHandlerForControl<T>,则需要小心事件处理程序中的代码;但是,无论事件处理程序中的代码如何,都可以安全地使用EventHandlerForControl<T>和EventHandlerForActiveControl<T>。大多数情况下,我使用EventHandlerForActiveControl<T>,因为更新已释放的表单没有意义。 - csharptest.net
您上面帖子中的方法可以在您不等待事件完成执行的情况下工作得足够好。尽管如此,您可以只调用BeginInvoke并捕获ObjectDisposedException以获得相同的结果。它之所以有效是因为您没有阻塞在事件完成上。如果您要在完成时阻塞,您的解决方案可能会导致客户端死锁。 - csharptest.net
2
@csharptest.net:“阅读BCL代码花费了一些时间…”是哪个BCL代码?MS .Net 1.1?MS .Net 4.5?MS .Net 6(我假设会有一个)?Mono是否以这种方式执行?如果明天创建一个新的框架,他们会这样做吗?这是未记录的行为,因此,如果框架以与文档一致但与您查看的实现不一致的方式实现此操作,则您的程序将中断。我的原因比你的好,因为它仅依赖于记录的行为。很好地挖掘了,但这是一个黑客,不适合大多数商业用途。 - lmat - Reinstate Monica
显然,你是正确的,这是一个hack/变通方法...而且很可能在Mono上不起作用。至于它适用于哪个版本的.NET,WinForms类在Microsoft的v2中被冻结,也就是这个程序编写的版本。声称你的解决方案在某种程度上“更好”是一种谬论。这是一个不同的解决方案,没有解决原帖中列出的7个约束条件中的任何一个。因此,说你的解决方案“更好”就像说苹果比橙子更好一样。你的解决方案有几个问题,但如果它符合你的需求,那就使用它吧。 - csharptest.net

1

我尝试将所有调用GUI的消息组织为fire and forget(处理GUI由于在释放窗体时出现竞争条件而可能抛出的异常)。

这样,如果它从未执行,就不会造成任何伤害。

如果GUI需要响应工作线程,则可以有效地反转通知。对于简单的需求,BackgroundWorker已经处理了这个问题。


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也可以使用类似的结构。


我之前提到过,根据http://msdn.microsoft.com/en-us/library/0b1bf3y3的规定,您不能在与创建控件的线程不同的线程上调用`IsDisposed`。实际上,由于这似乎是无法解决的问题,这可能是一个非常好的hack;但我们应该记住它只是一个hack(不遵循文档限制)。 - lmat - Reinstate Monica

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

因此,您始终可以使用invoke来确保您在正确的线程中。

3)注意同步调用。如果您想要,可以替换使用BeginInvoke而使用Invoke。这将阻塞您的组件,直到事件被处理。但是,如果在UI中您需要与仅限于组件所在线程的某些内容进行通信,则可能会出现死锁问题(我不知道我是否表达清楚,请告诉我)。我在使用反射(TargetInvocationException)和BeginInvoke时遇到了异常问题(因为它们启动了一个不同的线程,您会丢失部分堆栈跟踪),但我不记得在使用Invoke调用时遇到过很多麻烦,因此在异常方面应该是安全的。

哇,回答好长啊。如果我偶然错过了您的任何要求或误解了您说的话(英语不是我的母语,所以我们永远不确定),请告诉我。


2
第一点建议在关闭时取消订阅事件,但这并不是线程安全的。由于抢占式多线程,您无法确定在处理程序句柄被释放之前,您在其调用列表中的“存在”是否已经消失,因此,您不能确定他在处理程序句柄被释放后是否会使用您的回调函数。第二点是一个很好的提示。 - lmat - Reinstate Monica

0

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

同意,在这种情况下,人们可以主张在进程完成之前隐藏对话框;然而,我正在使用这个作为一种通用手段来讨论我在WinForms开发中普遍面临的更广泛的问题。你对这个特定问题很准确,但我不能将这个原则应用到所有情况。 - csharptest.net
这是一个很好的观察,但在任何应用程序不可避免的关闭阶段中都会失败。应用程序最终必须关闭,那个句柄将需要被处理,其他控件可能会调用您的窗体。隐藏窗体会一直隐藏问题,直到最后。 - lmat - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接