避免在跨线程WinForm事件处理中调用/开始调用的麻烦?

时间:2022-07-18 10:58:43

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

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

  1. Obviously the most important issue, I can not modify a Control unless I'm executing on the same thread that created it.
  2. 显然最重要的问题是,我不能修改控件,除非我在创建控件的同一个线程上执行。
  3. As you know, Invoke, BeginInvoke, etc are not available until after a Control is created.
  4. 如您所知,在创建控件之前,调用、BeginInvoke等方法都不可用。
  5. 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.
  6. 即使在RequiresInvoke返回true之后,BeginInvoke仍然可以抛出objectdispose,即使它不抛出,它也可能永远不会执行被销毁的代码。
  7. 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.
  8. 即使在RequiresInvoke返回true之后,调用仍然可以无限期地挂起等待在调用调用的同时被处理的控件执行。

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.

我正在寻找一个优雅的解决方案来解决这个问题,但是在我开始详细说明我在寻找什么之前,我想我应该先澄清这个问题。这是为了解决一般性问题,并在后面加上一个具体的例子。对于这个例子,假设我们正在internet上传输大量的数据。用户界面必须能够显示正在进行中的传输的进度对话框。进度对话框应该不断快速更新(每秒更新5到20次)。用户可以在任何时候取消进度对话框,如果需要,可以再次回忆它。此外,为了参数起见,我们假设如果对话框是可见的,那么它必须处理每个进度事件。用户可以单击进度对话框中的Cancel,通过修改事件args,取消操作。

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. 允许工作线程调用控件/窗体上的方法,并阻塞/等待执行完成。
  3. Allow the dialog itself to call this same method at initialization or the like (and thus not use invoke).
  4. 允许对话框本身在初始化或类似的时候调用相同的方法(因此不使用invoke)。
  5. Place no burden of implementation on the handling method or the calling event, the solution should only change the event subscription itself.
  6. 不给处理方法或调用事件增加实现负担,解决方案应该只更改事件订阅本身。
  7. 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.
  8. 适当地处理阻塞调用可能在处理过程中的对话框。不幸的是,这并不像检查isdispose那么容易。
  9. Must be able to be used with any event type (assume a delegate of type EventHandler)
  10. 必须能够与任何事件类型一起使用(假设类型为EventHandler)
  11. Must not translate exceptions to TargetInvocationException.
  12. 不能将异常转换为TargetInvocationException。
  13. The solution must work with .Net 2.0 and higher
  14. 解决方案必须与。net 2.0或更高版本兼容

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...

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

  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).

    控制。InvokeRequired =文档化,如果在当前线程上运行,则返回false;如果对所有父线程执行IsHandleCreated,则返回false。我被InvokeRequired实现所困扰,它可能会抛出objectdissedexception,甚至可能重新创建对象的句柄。既然InvokeRequired可以在我们不能调用(在进程中处理)时返回true,它也可以返回false,即使我们可能需要使用invoke(在进程中创建),这在所有情况下都是不可信的。我能看到的唯一可以信任InvokeRequired返回false的情况是,IsHandleCreated在调用前后都返回true(顺便说一句,invokerequerred的MSDN文档中确实提到了对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.

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

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

    控制。处理=在处理过程中返回true。

  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.
  5. 控制。如果控件已被处理,则isdispose =返回true。我正在考虑订阅已处理事件并检查isdispose属性以决定是否BeginInvoke将永远完成。这里的大问题是在配置->配置转换过程中缺少同步化锁。如果您订阅了已处理的事件,并在此之后验证了dispose == false && isdispose == false,那么您仍然可能不会看到已处理的事件触发。这是由于处理集处理= false,然后设置= true。这为您提供了一个机会(无论多么小)来读取处理和在处理控件上作为false处理。

... 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.

您将注意到ObjectDisposedException被分派到所有等待的跨线程调用。这是对this.window. destroyhandle()的调用,它反过来破坏了窗口,并设置了对IntPtr的处理。因此防止对BeginInvoke方法(或者更准确地说是MarshaledInvoke,它处理了BeginInvoke和Invoke)的进一步调用。这里的问题是,在threadCallbackList上的锁释放之后,可以在控件的线程将窗口句柄归零之前插入一个新条目。这似乎就是我所看到的情况,虽然很少,但足以阻止发布。

Update #4:

更新# 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'.

不好意思继续拖下去;然而,我认为它值得在这里记录。我已经成功地解决了上面的大多数问题,我正在缩小范围,寻找一个有效的解决方案。我还关注了另一个我担心的问题,但直到现在,我还没有看到《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:

这本身并不是那么糟糕(不管我对get{}修改的看法);但是,当与InvokeRequired属性或Invoke/BeginInvoke方法相结合时,它是不好的。下面是调用的基本流程:

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.

这里的问题是,从另一个线程中,我可以成功地通过第一个if语句,然后该句柄被该控件的线程破坏,从而导致handle属性的get在我的线程上重新创建窗口句柄。这将导致在原始控件的线程上引发异常。这件事真的让我很困惑,因为没有办法阻止它。如果他们只使用内部处理属性并测试IntPtr的结果。零,这不是问题。

13 个解决方案

#1


22  

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.

正如所描述的,您的场景非常适合背景工作者——为什么不直接使用它呢?您对解决方案的要求太笼统,而且相当不合理——我怀疑是否有任何解决方案可以满足它们。

#2


8  

I ran into this problem awhile back and came up with solution involving Synchronization Contexts. The solution is to add an extension method to SynchronizationContext which binds a particular delegate to the thread that the SynchronizationContext is bound to. It will generate a new delegate which when invoked will marshal the call to the appropraite thread and then call the original delegate. It makes it nearly impossible for consumers of the delegate to call it in the wrong context.

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

Blog post on the subject:

关于这个主题的博文:

#3


6  

Ok, days later I've finished creating a solution. It solves all of the listed constraints and objectives in the initial post. The usage is simple and straight-forward:

几天后,我完成了一个解决方案。它解决了最初职位中列出的所有约束和目标。使用方法简单明了:

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

When the worker thread calls this event it will handle the required invocation to the control thread. It ensures that it will not hang indefinitely and will consistently throw an ObjectDisposedException if it is unable to execute on the control thread. I've created other derivations of the class, one to ignore the error, and another to directly call the delegate if the control is not available. Appears to work well and fully passes the several tests that reproduce the issues above. There is only one issue with the solution I can't prevent without violating constraint #3 above. This issue is the last (Update #4) in the issue description, the threading problems in get Handle. This can cause unexpected behavior on the original control thread, and I've regularly seen InvalidOperationException() thrown while calling Dispose() since the handle in the process of being created on my thread. To allow for dealing with this I ensure a lock around accessing functions that will use the Control.Handle property. This allows a form to overload the DestroyHandle method and lock prior to calling the base implementation. If this is done, this class should be entirely thread-safe (To the best of my knowledge).

当工作线程调用此事件时,它将处理对控制线程所需的调用。它确保它不会无限期地挂起,并且如果无法在控制线程上执行,它将始终抛出ObjectDisposedException。我创建了类的其他派生,一个用于忽略错误,另一个用于在控件不可用时直接调用委托。看起来工作得很好,并且完全通过了重复上述问题的几个测试。这个解决方案只有一个问题,如果不违反上面的约束#3,我就无法阻止它。这个问题是问题描述中的最后一个问题(更新#4),即get句柄中的线程问题。这可能会在原始的控制线程上导致意外的行为,而且我经常看到在调用Dispose()时抛出InvalidOperationException(),因为在我的线程上创建进程中的句柄。为了处理这个问题,我确保了一个锁,用于访问将使用控件的函数。处理财产。这允许表单在调用基本实现之前重载驱逐舰句柄方法和锁。如果这样做了,这个类应该是完全线程安全的(就我所知而言)。

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

You may notice the core aspect of solving the dead-lock became a polling loop. Originally I successfully solved the test cases by handling the control's event for Disposed and HandleDestroyed and using multiple wait handles. After a more careful review, I found the subscription/unsubscription from these events is not thread-safe. Thus I chose to poll the IsHandleCreated instead so as not to create unneeded contention on the thread's events and thereby avoid the possibility of still producing a dead-lock state.

您可能注意到解决死锁的核心方面变成了轮询循环。最初,我成功地解决了测试用例,方法是处理控件的事件进行处理、销毁处理和使用多个等待句柄。仔细检查之后,我发现这些事件的订阅/取消订阅不是线程安全的。因此,我选择对ishandreated进行轮询,以避免在线程的事件上创建不必要的争用,从而避免仍然产生死锁状态的可能性。

Anyway, here is the solution I came up with:

不管怎样,这就是我提出的解决方案:

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

If you see anything wrong here, please let me know.

如果你发现这里有什么问题,请告诉我。

#4


2  

I'm not going to write an exhaustive solution for you that meets all of your requirements, but I will offer perspective. Overall, though, I think you're shooting for the moon with those requirements.

我不会为满足您所有需求的您编写一个详尽的解决方案,但是我将提供透视图。总的来说,我认为你是在为月球而努力。

The Invoke/BeginInvoke architecture simply executes a supplied delegate on the control's UI thread by sending it a Windows message and the message loop itself executes the delegate. The specific workings of this are irrelevant, but the point is that there is no particular reason that you have to use this architecture for thread sync with the UI thread. All you need is some other loop running, such as in a Forms.Timer or something like that, that monitors a Queue for delegates to execute and does so. It would be fairly simple to implement your own, though I don't know what specifically it would get for you that Invoke and BeginInvoke don't provide.

调用/BeginInvoke体系结构只是通过向控件的UI线程发送Windows消息来执行提供的委托,消息循环本身执行委托。它的具体工作是不相关的,但关键是您没有特别的理由必须使用这个体系结构来与UI线程进行线程同步。您所需要的只是运行其他循环,例如在窗体中。定时器或类似的东西,用于监视委托执行的队列并执行。实现您自己的会非常简单,尽管我不知道调用和BeginInvoke没有提供什么特殊功能。

#5


1  

This is not really answer to the second part of the question, but I will include it just for the reference:

这并不是对问题的第二部分的真正回答,但我将把它包括在这里作为参考:

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

This code should avoid the most common pitfalls with Invoke/BeginInvoke and it's easy to use . Just turn

这段代码应该可以避免调用/BeginInvoke最常见的缺陷,而且很容易使用。只是把

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

into

control.SafeInvoke(...)

Similar construct is possible for BeginInvoke.

类似的构造可以用于BeginInvoke。

#6


1  

Wow, long question. I'll try to organize my answer so you can correct me if I understood something wrong, ok?

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

1) Unless you have an extremely good reason to call UI methods directly from different threads, don't. You can always go for the producer/consumer model using event handlers:

1)除非您有非常好的理由直接从不同的线程调用UI方法,否则不要这样做。您可以使用事件处理程序来获取生产者/消费者模型:

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

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

myHandler will be fired every time the component in a different thread needs to perform something in the UI, for example. Also, setting up the event handler in OnLoad and unsubscribing in OnClosing makes sure that the events will only be received/handled by the UI while its handle is created and ready to process the events. You won't even be able to fire events to this dialog if it is in the process of disposing, because you won't be subscribed to the event anymore. If another event is fired while one is still being processed, it will be queued.

例如,每当不同线程中的组件需要在UI中执行某些操作时,myHandler就会被触发。此外,在OnLoad中设置事件处理程序,并在onclose中取消订阅,可以确保事件只在创建其句柄并准备处理事件时由UI接收/处理。如果在处理过程中,您甚至不能将事件触发到这个对话框中,因为您将不再订阅该事件。如果另一个事件在一个事件仍在处理时被触发,那么它将被排队。

You can pass all the info you need in the event arguments: whether you're updating the progress, closing the window, etc.

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

2) You don't need InvokeRequired if you use the model I suggested above. In this example, you know that the only thing that is firing myHandler will be your component that lives in another thread, for example.

如果您使用我上面建议的模型,您不需要InvokeRequired。在本例中,您知道惟一要触发myHandler的是位于另一个线程中的组件,例如。

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

So you can always use invoke to make sure you'll be in the right thread.

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

3) Beware of synchronous calls. If you want to, you can replace use Invoke instead of BeginInvoke. This will block your component until the event has been processed. However, if in the UI you need to communicate to something that is exclusive to the thread your component lives in, you can have deadlock problems. (I don't know if I made myself clear, please let me know). I've had problems with exceptions when using reflection (TargetInvocationException) and BeginInvoke (as they start a different thread, you lose part of the stack trace), but I don't recall having a lot of trouble with Invoke calls, so you should be safe when it comes to exceptions.

3)注意同步调用。如果您愿意,您可以替换使用Invoke而不是BeginInvoke。这将阻塞您的组件,直到事件被处理。但是,如果在UI中您需要与组件所在的线程进行通信,那么可能会出现死锁问题。(我不知道我说得是否清楚,请让我知道。)在使用反射(TargetInvocationException)和BeginInvoke(当它们启动不同的线程,您丢失了堆栈跟踪的一部分)时,我遇到了异常问题,但是我不记得调用调用有很多麻烦,所以在遇到异常时应该是安全的。

Whoa, long answer. If by any chance I missed any of your requirements or misunderstood something you said (english is not my native language, so we're never sure), please let me know.

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

#7


1  

I try to organize all such invoke messages to the GUI as fire and forget (handling the exception the GUI can throw due to the race condition on disposing the form).

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

This way if it never executes no harm is done.

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

If the GUI needs to respond to the working thread it has a way of effectively reversing the notification. For simple needs BackgroundWorker already handles this.

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

#8


1  

This is quite a difficult question. As I mention in a comment, I don't think it's solvable given the documented constraints. You can hack it given a particular implementation of the .net framework: knowing the implementation of various member functions may help you cheat by grabbing locks here and there, and knowing that "it's actually OKAY, to call other member functions on a different thread."

这是一个相当困难的问题。正如我在一篇评论中提到的,我认为考虑到文档化的约束,它是无法解决的。通过。net框架的特定实现,您可以破解它:了解各种成员函数的实现可以帮助您在这里和那里获取锁,并且知道“在不同的线程上调用其他成员函数实际上是可以的”。

So, my basic answer for now is, "no." I hate to say it's not possible because I have a great deal of faith in the .Net framework. Also, I'm comparatively a novice, not having studied frameworks in general, or CS, but the internet is open (even to ignorant people like me)!

所以,我现在的基本回答是,“不。”我不想说这是不可能的,因为我对。net框架很有信心。另外,我是一个相对新手,没有学习过一般的框架,或者CS,但是互联网是开放的(甚至对于像我这样无知的人)!

On a different topic, the argument can be posed and well-supported, "You should never need Invoke, only use BeginInvoke, and fire and forget." I won't bother trying to support it or even say that it's a correct assertion, but I will say that the common implementation is incorrect, and pose a working (I hope) one.

在另一个主题上,可以对参数进行定位并得到良好的支持,“您永远不应该需要调用,而应该只使用BeginInvoke,并使用fire和forget。”我不会费心去支持它,甚至不会说它是一个正确的断言,但我要说,通用实现是不正确的,并提出一个有效的(我希望)。

Here's a common implementation (taken from a different answer here):

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

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

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

This isn't thread safe. The component could have easily begun to call the invocation list just before the unsubscription, and only after we finish disposing does the handler get invoked. The real point is, it's not documented how each component must use the event mechanism in .Net, and honestly, he doesn't have to unsubscribe you at all: once you've given out your phone number, nobody's required to erase it!

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

Better is:

更好的是:

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;

Please let me know if I missed something.

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

#9


0  

If you don't like the BackgroundWoker (as described by @Pavel) you might want to look at this library http://www.wintellect.com/PowerThreading.aspx.

如果您不喜欢BackgroundWoker(如@Pavel所述),您可能需要查看这个库http://www.wintellectual t.com/powerthread.aspx。

#10


0  

If I understand this, why do you ever need to dispose the progress dialog while the application is running? Why not just show and hide it on the users request? This sounds like it will make your problem at least a little bit simpler.

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

#11


0  

Why not just hide the dialog when the user dismisses it? That should work fine if you don't show that dialog modally. (use show instead of showdialog). I believe that you can keep your progress dialog on top of your owning window (if you need to) by passing the host to the dialog when you call show.

当用户取消对话时,为什么不隐藏它呢?如果你不模态地显示那个对话框,那应该没问题。(用show代替showdialog)。我相信您可以通过在调用show时将主机传递给对话框,将进度对话框置于您的所属窗口之上(如果需要的话)。

#12


0  

Using System.ComponentModel.ISynchronizeInvoke is nice when creating a System.ComponentModel.Component, such as the BackgroundWorker. The following code snippet is how the FileSystemWater handles events.

使用System.ComponentModel。ISynchronizeInvoke用于创建system . component模型。组件,如后台工作程序。下面的代码片段是文件系统水处理事件的方式。

    ''' <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


0  

Here's what I'm currently using. It's based on the use of SynchronizationContext, and was inspired by JaredPar's blog article - see his answer above. This may not be perfect, but it does avoid some of the OP's problems that I was also experiencing.

这是我目前使用的。它基于同步上下文的使用,并受到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://*.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);
      }
   }

#1


22  

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.

正如所描述的,您的场景非常适合背景工作者——为什么不直接使用它呢?您对解决方案的要求太笼统,而且相当不合理——我怀疑是否有任何解决方案可以满足它们。

#2


8  

I ran into this problem awhile back and came up with solution involving Synchronization Contexts. The solution is to add an extension method to SynchronizationContext which binds a particular delegate to the thread that the SynchronizationContext is bound to. It will generate a new delegate which when invoked will marshal the call to the appropraite thread and then call the original delegate. It makes it nearly impossible for consumers of the delegate to call it in the wrong context.

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

Blog post on the subject:

关于这个主题的博文:

#3


6  

Ok, days later I've finished creating a solution. It solves all of the listed constraints and objectives in the initial post. The usage is simple and straight-forward:

几天后,我完成了一个解决方案。它解决了最初职位中列出的所有约束和目标。使用方法简单明了:

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

When the worker thread calls this event it will handle the required invocation to the control thread. It ensures that it will not hang indefinitely and will consistently throw an ObjectDisposedException if it is unable to execute on the control thread. I've created other derivations of the class, one to ignore the error, and another to directly call the delegate if the control is not available. Appears to work well and fully passes the several tests that reproduce the issues above. There is only one issue with the solution I can't prevent without violating constraint #3 above. This issue is the last (Update #4) in the issue description, the threading problems in get Handle. This can cause unexpected behavior on the original control thread, and I've regularly seen InvalidOperationException() thrown while calling Dispose() since the handle in the process of being created on my thread. To allow for dealing with this I ensure a lock around accessing functions that will use the Control.Handle property. This allows a form to overload the DestroyHandle method and lock prior to calling the base implementation. If this is done, this class should be entirely thread-safe (To the best of my knowledge).

当工作线程调用此事件时,它将处理对控制线程所需的调用。它确保它不会无限期地挂起,并且如果无法在控制线程上执行,它将始终抛出ObjectDisposedException。我创建了类的其他派生,一个用于忽略错误,另一个用于在控件不可用时直接调用委托。看起来工作得很好,并且完全通过了重复上述问题的几个测试。这个解决方案只有一个问题,如果不违反上面的约束#3,我就无法阻止它。这个问题是问题描述中的最后一个问题(更新#4),即get句柄中的线程问题。这可能会在原始的控制线程上导致意外的行为,而且我经常看到在调用Dispose()时抛出InvalidOperationException(),因为在我的线程上创建进程中的句柄。为了处理这个问题,我确保了一个锁,用于访问将使用控件的函数。处理财产。这允许表单在调用基本实现之前重载驱逐舰句柄方法和锁。如果这样做了,这个类应该是完全线程安全的(就我所知而言)。

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

You may notice the core aspect of solving the dead-lock became a polling loop. Originally I successfully solved the test cases by handling the control's event for Disposed and HandleDestroyed and using multiple wait handles. After a more careful review, I found the subscription/unsubscription from these events is not thread-safe. Thus I chose to poll the IsHandleCreated instead so as not to create unneeded contention on the thread's events and thereby avoid the possibility of still producing a dead-lock state.

您可能注意到解决死锁的核心方面变成了轮询循环。最初,我成功地解决了测试用例,方法是处理控件的事件进行处理、销毁处理和使用多个等待句柄。仔细检查之后,我发现这些事件的订阅/取消订阅不是线程安全的。因此,我选择对ishandreated进行轮询,以避免在线程的事件上创建不必要的争用,从而避免仍然产生死锁状态的可能性。

Anyway, here is the solution I came up with:

不管怎样,这就是我提出的解决方案:

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

If you see anything wrong here, please let me know.

如果你发现这里有什么问题,请告诉我。

#4


2  

I'm not going to write an exhaustive solution for you that meets all of your requirements, but I will offer perspective. Overall, though, I think you're shooting for the moon with those requirements.

我不会为满足您所有需求的您编写一个详尽的解决方案,但是我将提供透视图。总的来说,我认为你是在为月球而努力。

The Invoke/BeginInvoke architecture simply executes a supplied delegate on the control's UI thread by sending it a Windows message and the message loop itself executes the delegate. The specific workings of this are irrelevant, but the point is that there is no particular reason that you have to use this architecture for thread sync with the UI thread. All you need is some other loop running, such as in a Forms.Timer or something like that, that monitors a Queue for delegates to execute and does so. It would be fairly simple to implement your own, though I don't know what specifically it would get for you that Invoke and BeginInvoke don't provide.

调用/BeginInvoke体系结构只是通过向控件的UI线程发送Windows消息来执行提供的委托,消息循环本身执行委托。它的具体工作是不相关的,但关键是您没有特别的理由必须使用这个体系结构来与UI线程进行线程同步。您所需要的只是运行其他循环,例如在窗体中。定时器或类似的东西,用于监视委托执行的队列并执行。实现您自己的会非常简单,尽管我不知道调用和BeginInvoke没有提供什么特殊功能。

#5


1  

This is not really answer to the second part of the question, but I will include it just for the reference:

这并不是对问题的第二部分的真正回答,但我将把它包括在这里作为参考:

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

This code should avoid the most common pitfalls with Invoke/BeginInvoke and it's easy to use . Just turn

这段代码应该可以避免调用/BeginInvoke最常见的缺陷,而且很容易使用。只是把

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

into

control.SafeInvoke(...)

Similar construct is possible for BeginInvoke.

类似的构造可以用于BeginInvoke。

#6


1  

Wow, long question. I'll try to organize my answer so you can correct me if I understood something wrong, ok?

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

1) Unless you have an extremely good reason to call UI methods directly from different threads, don't. You can always go for the producer/consumer model using event handlers:

1)除非您有非常好的理由直接从不同的线程调用UI方法,否则不要这样做。您可以使用事件处理程序来获取生产者/消费者模型:

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

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

myHandler will be fired every time the component in a different thread needs to perform something in the UI, for example. Also, setting up the event handler in OnLoad and unsubscribing in OnClosing makes sure that the events will only be received/handled by the UI while its handle is created and ready to process the events. You won't even be able to fire events to this dialog if it is in the process of disposing, because you won't be subscribed to the event anymore. If another event is fired while one is still being processed, it will be queued.

例如,每当不同线程中的组件需要在UI中执行某些操作时,myHandler就会被触发。此外,在OnLoad中设置事件处理程序,并在onclose中取消订阅,可以确保事件只在创建其句柄并准备处理事件时由UI接收/处理。如果在处理过程中,您甚至不能将事件触发到这个对话框中,因为您将不再订阅该事件。如果另一个事件在一个事件仍在处理时被触发,那么它将被排队。

You can pass all the info you need in the event arguments: whether you're updating the progress, closing the window, etc.

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

2) You don't need InvokeRequired if you use the model I suggested above. In this example, you know that the only thing that is firing myHandler will be your component that lives in another thread, for example.

如果您使用我上面建议的模型,您不需要InvokeRequired。在本例中,您知道惟一要触发myHandler的是位于另一个线程中的组件,例如。

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

So you can always use invoke to make sure you'll be in the right thread.

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

3) Beware of synchronous calls. If you want to, you can replace use Invoke instead of BeginInvoke. This will block your component until the event has been processed. However, if in the UI you need to communicate to something that is exclusive to the thread your component lives in, you can have deadlock problems. (I don't know if I made myself clear, please let me know). I've had problems with exceptions when using reflection (TargetInvocationException) and BeginInvoke (as they start a different thread, you lose part of the stack trace), but I don't recall having a lot of trouble with Invoke calls, so you should be safe when it comes to exceptions.

3)注意同步调用。如果您愿意,您可以替换使用Invoke而不是BeginInvoke。这将阻塞您的组件,直到事件被处理。但是,如果在UI中您需要与组件所在的线程进行通信,那么可能会出现死锁问题。(我不知道我说得是否清楚,请让我知道。)在使用反射(TargetInvocationException)和BeginInvoke(当它们启动不同的线程,您丢失了堆栈跟踪的一部分)时,我遇到了异常问题,但是我不记得调用调用有很多麻烦,所以在遇到异常时应该是安全的。

Whoa, long answer. If by any chance I missed any of your requirements or misunderstood something you said (english is not my native language, so we're never sure), please let me know.

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

#7


1  

I try to organize all such invoke messages to the GUI as fire and forget (handling the exception the GUI can throw due to the race condition on disposing the form).

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

This way if it never executes no harm is done.

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

If the GUI needs to respond to the working thread it has a way of effectively reversing the notification. For simple needs BackgroundWorker already handles this.

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

#8


1  

This is quite a difficult question. As I mention in a comment, I don't think it's solvable given the documented constraints. You can hack it given a particular implementation of the .net framework: knowing the implementation of various member functions may help you cheat by grabbing locks here and there, and knowing that "it's actually OKAY, to call other member functions on a different thread."

这是一个相当困难的问题。正如我在一篇评论中提到的,我认为考虑到文档化的约束,它是无法解决的。通过。net框架的特定实现,您可以破解它:了解各种成员函数的实现可以帮助您在这里和那里获取锁,并且知道“在不同的线程上调用其他成员函数实际上是可以的”。

So, my basic answer for now is, "no." I hate to say it's not possible because I have a great deal of faith in the .Net framework. Also, I'm comparatively a novice, not having studied frameworks in general, or CS, but the internet is open (even to ignorant people like me)!

所以,我现在的基本回答是,“不。”我不想说这是不可能的,因为我对。net框架很有信心。另外,我是一个相对新手,没有学习过一般的框架,或者CS,但是互联网是开放的(甚至对于像我这样无知的人)!

On a different topic, the argument can be posed and well-supported, "You should never need Invoke, only use BeginInvoke, and fire and forget." I won't bother trying to support it or even say that it's a correct assertion, but I will say that the common implementation is incorrect, and pose a working (I hope) one.

在另一个主题上,可以对参数进行定位并得到良好的支持,“您永远不应该需要调用,而应该只使用BeginInvoke,并使用fire和forget。”我不会费心去支持它,甚至不会说它是一个正确的断言,但我要说,通用实现是不正确的,并提出一个有效的(我希望)。

Here's a common implementation (taken from a different answer here):

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

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

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

This isn't thread safe. The component could have easily begun to call the invocation list just before the unsubscription, and only after we finish disposing does the handler get invoked. The real point is, it's not documented how each component must use the event mechanism in .Net, and honestly, he doesn't have to unsubscribe you at all: once you've given out your phone number, nobody's required to erase it!

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

Better is:

更好的是:

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;

Please let me know if I missed something.

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

#9


0  

If you don't like the BackgroundWoker (as described by @Pavel) you might want to look at this library http://www.wintellect.com/PowerThreading.aspx.

如果您不喜欢BackgroundWoker(如@Pavel所述),您可能需要查看这个库http://www.wintellectual t.com/powerthread.aspx。

#10


0  

If I understand this, why do you ever need to dispose the progress dialog while the application is running? Why not just show and hide it on the users request? This sounds like it will make your problem at least a little bit simpler.

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

#11


0  

Why not just hide the dialog when the user dismisses it? That should work fine if you don't show that dialog modally. (use show instead of showdialog). I believe that you can keep your progress dialog on top of your owning window (if you need to) by passing the host to the dialog when you call show.

当用户取消对话时,为什么不隐藏它呢?如果你不模态地显示那个对话框,那应该没问题。(用show代替showdialog)。我相信您可以通过在调用show时将主机传递给对话框,将进度对话框置于您的所属窗口之上(如果需要的话)。

#12


0  

Using System.ComponentModel.ISynchronizeInvoke is nice when creating a System.ComponentModel.Component, such as the BackgroundWorker. The following code snippet is how the FileSystemWater handles events.

使用System.ComponentModel。ISynchronizeInvoke用于创建system . component模型。组件,如后台工作程序。下面的代码片段是文件系统水处理事件的方式。

    ''' <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


0  

Here's what I'm currently using. It's based on the use of SynchronizationContext, and was inspired by JaredPar's blog article - see his answer above. This may not be perfect, but it does avoid some of the OP's problems that I was also experiencing.

这是我目前使用的。它基于同步上下文的使用,并受到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://*.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);
      }
   }