【问题标题】:Avoiding the woes of Invoke/BeginInvoke in cross-thread WinForm event handling?在跨线程 WinForm 事件处理中避免 Invoke/BeginInvoke 的麻烦?
【发布时间】:2010-11-24 17:56:39
【问题描述】:

我仍然被 WinForm UI 中的后台线程所困扰。为什么?以下是一些问题:

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

我正在寻找一个优雅的解决方案来解决这个问题,但在我详细了解我正在寻找的内容之前,我想我会澄清这个问题。这是为了解决一般问题,并在其背后放置一个更具体的例子。对于此示例,假设我们正在通过 Internet 传输大量数据。用户界面必须能够显示正在进行的传输的进度对话框。进度对话框应不断快速更新(每秒更新 5 到 20 次)。用户可以随时关闭进度对话框并在需要时再次调用它。此外,为了争论,假设对话框是可见的,它必须处理每个进度事件。用户可以在进度对话框中点击取消,通过修改事件参数,取消操作。

现在我需要一个适合以下约束框的解决方案:

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

那么,考虑到上述限制,这可以解决吗?我已经搜索和挖掘了无数的博客和讨论,可惜我还是两手空空。

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

更新 #2:好的,我将尝试更详细地描述问题,看看有什么问题(如果有的话)。以下允许我们确定其状态的属性有几件事引起关注...

  1. Control.InvokeRequired = 如果在当前线程上运行或 IsHandleCreated 为所有父级返回 false,则记录为返回 false。 我对 InvokeRequired 实现有可能抛出 ObjectDisposedException 甚至可能重新创建对象的句柄感到困扰。而且由于 InvokeRequired 可以在我们无法调用(正在处理中)时返回 true,并且即使我们可能需要使用调用(正在创建中),它也可以返回 false,这在所有情况下都不能被信任。我可以看到我们可以信任 InvokeRequired 返回 false 的唯一情况是 IsHandleCreated 在调用前后都返回 true (顺便说一句,InvokeRequired 的 MSDN 文档确实提到了检查 IsHandleCreated)。

  2. Control.IsHandleCreated = 如果已将句柄分配给控件,则返回 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();
}

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

更新#4:

很抱歉一直拖下去;但是,我认为值得在这里记录。我已经设法解决了上述大部分问题,并且正在缩小范围内的解决方案。我又遇到了一个我担心的问题,但直到现在,还没有看到'in-the-wild'。

这个问题与编写 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 方法结合使用时,它就很糟糕了。这是调用的基本流程:

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

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

【问题讨论】:

  • 提问的时候可以客气一点。

标签: c# .net winforms multithreading events


【解决方案1】:

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

【讨论】:

  • 我同意你的观点,这是一个难题。相信很多人都遇到过。我提出这个问题的原因是我不相信有解决办法;但是,我希望有人能证明我错了。
  • Pavel,感谢您在 BackgroundWorker 指导我,我不知道它的存在。它确实非常适合我描述的场景,我一定会找到它的用途。
  • 它并不优雅,但我想您会喜欢我发布的解决方案,它可以在不受 Invoke、BeginInvoke 和 BackgroundWorker 都产生的线程问题困扰的情况下工作。如果您可以确定您的表单在完成之前不会关闭,并且表单不会重新创建它的句柄,BackgroundWorker 仍然可以很好地完成这项工作。
  • 在搜索博客和新闻组以及其他“垃圾”之前...搜索对象浏览器。我听说 .NET Framework 中有超过 70,000 个对象可供选择。如果你找不到你要找的东西,你会找到一些项目来帮助你更好地搜索其他“垃圾”。
【解决方案2】:

我不久前遇到了这个问题,并提出了涉及同步上下文的解决方案。解决方案是向 SynchronizationContext 添加一个扩展方法,该方法将特定委托绑定到 SynchronizationContext 绑定到的线程。它将生成一个新的委托,该委托在被调用时会将调用编组到适当的线程,然后调用原始委托。这使得委托的消费者几乎不可能在错误的上下文中调用它。

关于该主题的博文:

【讨论】:

  • 一个不错的解决方案,通过一些工作它可以避免 DynamicInvoke 调用,因此不会生成 TargetInvocationException。总而言之,我喜欢这种方法,但我有一个问题:生成方法的开销是多少?不再使用时它们会被卸载吗?此外,我仍然有点困惑为什么您选择方法生成方法而不是包装委托的简单类。
  • 非常有趣的博客文章。一个小的挑剔点:你提到“ISynchronizedInvoke”五次。我假设您的意思是 ISynchronizeInvoke?
  • 您写道,“动态创建委托实例并不简单。除非我们将委托签名的所有排列编码到一个类中,否则我们无法使用 Delegate.Create API,因为我们无法提供具有匹配的签名。”使用泛型的 Action 构造,不能很大程度上减少这个问题吗?那么你只需要为接受 0 个参数的方法、接受 1 个参数的方法、接受 2 个参数的方法提供支持,这可能就足够了。还是我完全误解了什么?
【解决方案3】:

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

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

当工作线程调用此事件时,它将处理对控制线程所需的调用。它确保它不会无限期挂起,并且如果它无法在控制线程上执行,它将始终抛出 ObjectDisposedException。我创建了该类的其他派生,一个用于忽略错误,另一个用于在控件不可用时直接调用委托。似乎运行良好并完全通过了重现上述问题的几个测试。在不违反上述约束 #3 的情况下,我无法阻止的解决方案只有一个问题。此问题是问题描述中的最后一个(更新 #4),即 get Handle 中的线程问题。这可能会导致原始控制线程出现意外行为,并且我经常看到 InvalidOperationException() 在调用 Dispose() 时抛出,因为在我的线程上创建了句柄。为了处理这个问题,我确保锁定访问将使用 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; } }
    }
}

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

【讨论】:

  • 谁有更好的解决方案?我会在几天内发布赏金,看看会发生什么。
  • 如果您仍然感兴趣,请查看:stackoverflow.com/questions/4190299/…
  • 阅读完所有内容后,您仍然错过了 MSDN 中最重要的内容:“除了InvokeRequired 属性,控件上还有四种线程安全的方法:@987654326 @、BeginInvokeEndInvokeCreateGraphics 如果控件的句柄已经创建。在后台线程上创建控件句柄之前调用 CreateGraphics 可能会导致非法的跨线程调用。"跨度>
  • 换句话说,您不能从不同的线程调用IsHandleCreatedGetType。调查这个永久问题的出色工作。我认为这不是可以解决的。该模型存在无法通过记录的限制克服的问题。
  • 你说,“避免与 ReCreateHandle 冲突”。这如何避免与ReCreateHandle 发生冲突?我猜你假设ReCreateHandle locks 在this 上?这在某处有记录吗?
【解决方案4】:

我不会为您编写满足您所有要求的详尽解决方案,但我会提供观点。不过,总的来说,我认为您正在满足这些要求。

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

【讨论】:

  • 我完全同意,这是/一直是我对大多数事情的偏好(一个简单的生产者/消费者队列)。我对这个解决方案的问题是双重的。 A)它需要对客户端代码(在winform中运行的代码)产生相当大的影响; B)它不容易允许阻塞/双向事件。当然可以,创建一个等待句柄,等待消费者处理完消息,然后继续;但是,这会让您回到开始时的同一条船上......处理用户处理 UI 的潜力。
  • 在某种程度上,无论好坏,你都会留下来。正如其他人所建议的那样,当用户关闭进度对话框时,您不必实际处理它。
【解决方案5】:

这不是问题第二部分的真正答案,但我将其包括在内仅供参考:

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

【讨论】:

  • 我在上面提到,根据msdn.microsoft.com/en-us/library/0b1bf3y3,您不能在与创建控件的线程不同的线程上调用IsDisposed。在实践中,由于这似乎无法解决,这可能是一个非常好的 hack;但我们应该记住,这是一种 hack(不遵守记录的限制)。
【解决方案6】:

哇,好长的问题。 我会尽量整理我的答案,如果我理解错了,你可以纠正我,好吗?

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 调用有很多麻烦,所以你应该遇到异常时要安全。

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

【讨论】:

  • 第一点建议在您关闭时取消订阅该事件,但这不是线程安全的。由于抢占式多线程,您无法确定他的调用列表中的“您的”存在在句柄被释放之前已经消失,因此,您无法确定他不会使用处理完句柄后的回调。第 2 点的出色说明。
【解决方案7】:

我尝试将所有此类到 GUI 的调用消息组织为“一劳永逸”(处理由于处理表单时的竞争条件而导致 GUI 可能抛出的异常)。

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

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

【讨论】:

    【解决方案8】:

    这是一个相当困难的问题。正如我在评论中提到的那样,鉴于记录在案的限制,我认为这是不可解决的。您可以根据 .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;
    

    如果我遗漏了什么,请告诉我。

    【讨论】:

    • 前段时间为我解决了这个问题,csharptest.net/browse/src/Library/Delegates/… 你需要做的就是在调用 base 之前重写 DestroyHandle() 以锁定(this),然后更改订阅事件的方式正如我对这篇文章的回答所证明的那样。
    • @csharptest.net 你怎么知道这行得通?它是否记录在某处?
    • 花了一些时间阅读 BCL 代码、剖析问题并实施解决方案。在使用它多年之后,无论是在生产中还是在一组严格的自动化测试中,我对它的行为都非常有信心。如果您使用 ForcedEventHandlerForControl 您需要注意事件处理程序中的代码;但是,无论事件处理程序中的代码如何,都可以安全地使用 EventHandlerForControl 和 EventHandlerForActiveControl。大多数时候我使用 EventHandlerForActiveControl 因为更新已处理的表单没有意义。
    • 鉴于您不等待事件完成执行,上述帖子中的方法可以很好地工作。虽然,您可以调用 BeginInvoke 并捕获 ObjectDisposedException 并获得相同的结果。它起作用的原因是您没有阻止事件的完成。如果您在完成时阻止,您的解决方案可能会死锁客户端。
    • @csharptest.net "读取 BCL 代码需要一些时间..." 哪个 BCL 代码? MS .Net 1.1? MS .Net 4.5? MS .Net 6(我想会有一个)? Mono是这样做的吗?如果明天创建一个新框架,他们会这样做吗?这是未记录的行为,因此如果框架以与文档一致的方式实现这一点,但与您查看的实现不一致,您的程序就会中断。我的比你的更好的原因是因为它依赖于记录的行为。很好的挖掘工作,但它是一种 hack,不适合大多数商业目的。
    【解决方案9】:

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

    【讨论】:

      【解决方案10】:

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

      【讨论】:

      • 同意,在这种情况下,人们可能会争辩说只是隐藏对话框,直到过程完成;但是,我将其用作讨论我在 WinForms 开发中普遍面临的更广泛问题的通用方法。您对这个特定问题的看法,但我不能将主体应用于所有情况。
      • 这是一个很好的观察,但由于任何应用程序不可避免的关闭阶段,它失败了。应用程序最终必须关闭,并且需要释放该句柄,并且其他控件可能会调用您的表单。隐藏表单将问题隐藏到最后。
      【解决方案11】:

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

      【讨论】:

      • 隐藏对话框隐藏了问题(请参阅我对 alexD 的评论)。
      • 对不起——也许我遗漏了什么。这是否处理引用另一个进程拥有的东西?当您的进程关闭时,该进程拥有的所有资源都将被拆除。我认为只有持有某种进程外资源才会有问题。
      • 我指的句柄就是Control.Handle。所以,问题是最终,对话需要Close()ed,并且可能仍会收到对BeginInvoke的请求,并且必须优雅地响应。
      【解决方案12】:

      在创建System.ComponentModel.Component 时使用System.ComponentModel.ISynchronizeInvoke 非常好,例如BackgroundWorker。以下代码 sn -p 是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
      

      【讨论】:

        【解决方案13】:

        这是我目前正在使用的。它基于 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);
              }
           }
        

        【讨论】:

          猜你喜欢
          • 2014-01-22
          • 1970-01-01
          • 1970-01-01
          • 2017-06-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多