csh*_*net 48 .net c# events multithreading winforms
我仍然受到WinForm UI中背景线程的困扰.为什么?以下是一些问题:
我正在寻找这个问题的优雅解决方案,但在我了解我正在寻找的具体内容之前,我想我会澄清问题.这是为了解决一般性问题并在其背后提出一个更具体的例子.对于这个例子,假设我们正在通过互联网传输大量数据.用户界面必须能够显示正在进行的传输的进度对话框.进度对话框应该持续快速更新(每秒更新5到20次).用户可以随时关闭进度对话框,并在需要时再次调用它.而且,让我们假装参数,如果对话框可见,它必须处理每个进度事件.用户可以单击进度对话框上的取消,并通过修改事件参数,取消操作.
现在我需要一个适合以下限制条件的解决方案:
那么,鉴于上述限制,这可以解决吗?我搜索并挖掘了无数的博客和讨论,唉,我还是空手而归.
更新:我确实意识到这个问题没有简单的答案.我只在这个网站上呆了几天,我见过一些有很多回答问题经验的人.我希望这些人中的一个能够充分解决这个问题,这样我就不会花费一周时间来建立一个合理的解决方案.
更新#2:好的,我将尝试更详细地描述问题,看看有什么(如果有的话)震动.允许我们确定其状态的以下属性引起了一些问题...
Control.InvokeRequired =记录如果在当前线程上运行或者IsHandleCreated为所有父项返回false,则返回false.我很担心InvokeRequired实现有可能抛出ObjectDisposedException或甚至可能重新创建对象的句柄.并且由于InvokeRequired在我们无法调用(正在进行Dispose)时可以返回true,并且它可以返回false,即使我们可能需要使用invoke(正在创建),这在所有情况下都不可信任.唯一可以看到我们可以信任的地方InvokeRequired返回false是当IsHandleCreated在调用之前和之后都返回true时(BTW,InvokeRequired的MSDN文档确实提到了对IsHandleCreated的检查).
Control.IsHandleCreated =如果已为控件分配了句柄,则返回true;否则返回true.否则,错误.虽然IsHandleCreated是一个安全的调用,但如果控件正在重新创建它的句柄,它可能会崩溃.这个潜在的问题似乎可以通过在访问IsHandleCreated和InvokeRequired时执行锁定(控制)来解决.
Control.Disposing =如果控件处于处理过程中,则返回true.
......我的头疼了:(希望上面的信息可以为那些遇到这些麻烦的人提供更多的解决方案.我很感激你的思考周期.
关闭麻烦......以下是Control.DestroyHandle()方法的后半部分:
if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
lock (this.threadCallbackList)
{
Exception exception = new ObjectDisposedException(base.GetType().Name);
while (this.threadCallbackList.Count > 0)
{
ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
entry.exception = exception;
entry.Complete();
}
}
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
this.window.DestroyHandle();
}
Run Code Online (Sandbox Code Playgroud)
您会注意到ObjectDisposedException被调度到所有等待的跨线程调用.紧接着是对this.window.DestroyHandle()的调用,它反过来破坏窗口并设置它对IntPtr.Zero的句柄引用,从而防止进一步调用BeginInvoke方法(或更准确地说是MarshaledInvoke,它同时处理BeginInvoke和Invoke).这里的问题是在threadCallbackList上发布锁之后,可以在Control的线程将窗口句柄归零之前插入一个新条目.这似乎是我看到的情况,虽然很少,但通常足以阻止释放.
更新#4:
很抱歉继续拖动它; 但是,我认为值得记录在这里.我已经设法解决了上面的大多数问题,我正在缩小有效的解决方案.我已经打了一个我担心的问题,但直到现在,还没有看到'在野外'.
这个问题与编写Control.Handle属性的天才有关:
public IntPtr get_Handle()
{
if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
{
throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
}
if (!this.IsHandleCreated)
{
this.CreateHandle();
}
return this.HandleInternal;
}
Run Code Online (Sandbox Code Playgroud)
这本身并不是那么糟糕(无论我对get {}修改的意见如何); 但是,当与InvokeRequired属性或Invoke/BeginInvoke方法结合使用时,它很糟糕.这是Invoke的基本流程:
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
Run Code Online (Sandbox Code Playgroud)
这里的问题是,从另一个线程我可以成功地传递第一个if语句,之后句柄被控件的线程销毁,从而导致Handle属性的get在我的线程上重新创建窗口句柄.这可能会导致在原始控件的线程上引发异常.这个真的让我难过,因为没有办法防范这一点.如果他们只使用InternalHandle属性并测试IntPtr.Zero的结果,这不会是一个问题.
我在一段时间后遇到了这个问题,并提出了涉及同步上下文的解决方案.解决方案是向SynchronizationContext添加扩展方法,该方法将特定委托绑定到SynchronizationContext绑定的线程.它将生成一个新的委托,在调用时将封送对appropraite线程的调用,然后调用原始委托.它使代表的消费者几乎不可能在错误的上下文中调用它.
关于这个主题的博客文章:
好的,几天后我已经完成了一个解决方案.它解决了初始帖子中列出的所有约束和目标.用法简单明了:
myWorker.SomeEvent += new EventHandlerForControl<EventArgs>(this, myWorker_SomeEvent).EventHandler;
Run Code Online (Sandbox Code Playgroud)
当工作线程调用此事件时,它将处理对控制线程的所需调用.它确保它不会无限期挂起,如果无法在控制线程上执行,它将始终抛出ObjectDisposedException.我已经创建了该类的其他派生,一个用于忽略错误,另一个用于在控件不可用时直接调用委托.似乎运作良好,并完全通过了几个重现上述问题的测试.在不违反上述约束#3的情况下,我无法阻止解决方案只有一个问题.这个问题是问题描述中的最后一个(Update#4),get Handle中的线程问题.这可能会导致原始控制线程出现意外行为,并且我经常看到在调用Dispose()时抛出的InvalidOperationException(),因为在我的线程上创建过程中的句柄.为了允许处理这个问题,我确保锁定访问将使用Control.Handle属性的函数.这允许表单在调用基本实现之前重载DestroyHandle方法并锁定.如果这样做,这个类应该完全是线程安全的(据我所知).
public class Form : System.Windows.Forms.Form
{
protected override void DestroyHandle()
{
lock (this) base.DestroyHandle();
}
}
Run Code Online (Sandbox Code Playgroud)
你可能会注意到解决死锁的核心方面变成了一个轮询循环.最初我通过处理Disposed和HandleDestroyed的控件事件并使用多个等待句柄成功解决了测试用例.经过仔细审查后,我发现这些事件的订阅/取消订阅不是线程安全的.因此,我选择轮询IsHandleCreated,以免在线程的事件上产生不必要的争用,从而避免仍然产生死锁状态的可能性.
无论如何,这是我提出的解决方案:
/// <summary>
/// Provies a wrapper type around event handlers for a control that are safe to be
/// used from events on another thread. If the control is not valid at the time the
/// delegate is called an exception of type ObjectDisposedExcpetion will be raised.
/// </summary>
[System.Diagnostics.DebuggerNonUserCode]
public class EventHandlerForControl<TEventArgs> where TEventArgs : EventArgs
{
/// <summary> The control who's thread we will use for the invoke </summary>
protected readonly Control _control;
/// <summary> The delegate to invoke on the control </summary>
protected readonly EventHandler<TEventArgs> _delegate;
/// <summary>
/// Constructs an EventHandler for the specified method on the given control instance.
/// </summary>
public EventHandlerForControl(Control control, EventHandler<TEventArgs> handler)
{
if (control == null) throw new ArgumentNullException("control");
_control = control.TopLevelControl;
if (handler == null) throw new ArgumentNullException("handler");
_delegate = handler;
}
/// <summary>
/// Constructs an EventHandler for the specified delegate converting it to the expected
/// EventHandler<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<T> type for easier use.
/// </summary>
public static implicit operator EventHandler<TEventArgs>(EventHandlerForControl<TEventArgs> instance)
{ return instance.EventHandler; }
/// <summary>
/// Handles the 'magic' of safely invoking the delegate on the control without producing
/// a dead-lock.
/// </summary>
public void EventHandler(object sender, TEventArgs args)
{
bool requiresInvoke = false, hasHandle = false;
try
{
lock (_control) // locked to avoid conflicts with RecreateHandle and DestroyHandle
{
if (true == (hasHandle = _control.IsHandleCreated))
{
requiresInvoke = _control.InvokeRequired;
// must remain true for InvokeRequired to be dependable
hasHandle &= _control.IsHandleCreated;
}
}
}
catch (ObjectDisposedException)
{
requiresInvoke = hasHandle = false;
}
if (!requiresInvoke && hasHandle) // control is from the current thread
{
_delegate(sender, args);
return;
}
else if (hasHandle) // control invoke *might* work
{
MethodInvokerImpl invocation = new MethodInvokerImpl(_delegate, sender, args);
IAsyncResult result = null;
try
{
lock (_control)// locked to avoid conflicts with RecreateHandle and DestroyHandle
result = _control.BeginInvoke(invocation.Invoker);
}
catch (InvalidOperationException)
{ }
try
{
if (result != null)
{
WaitHandle handle = result.AsyncWaitHandle;
TimeSpan interval = TimeSpan.FromSeconds(1);
bool complete = false;
while (!complete && (invocation.MethodRunning || _control.IsHandleCreated))
{
if (invocation.MethodRunning)
complete = handle.WaitOne();//no need to continue polling once running
else
complete = handle.WaitOne(interval);
}
if (complete)
{
_control.EndInvoke(result);
return;
}
}
}
catch (ObjectDisposedException ode)
{
if (ode.ObjectName != _control.GetType().Name)
throw;// *likely* from some other source...
}
}
OnControlDisposed(sender, args);
}
/// <summary>
/// The class is used to take advantage of a special-case in the Control.InvokeMarshaledCallbackDo()
/// implementation that allows us to preserve the exception types that are thrown rather than doing
/// a delegate.DynamicInvoke();
/// </summary>
[System.Diagnostics.DebuggerNonUserCode]
private class MethodInvokerImpl
{
readonly EventHandler<TEventArgs> _handler;
readonly object _sender;
readonly TEventArgs _args;
private bool _received;
public MethodInvokerImpl(EventHandler<TEventArgs> handler, object sender, TEventArgs args)
{
_received = false;
_handler = handler;
_sender = sender;
_args = args;
}
public MethodInvoker Invoker { get { return this.Invoke; } }
private void Invoke() { _received = true; _handler(_sender, _args); }
public bool MethodRunning { get { return _received; } }
}
}
Run Code Online (Sandbox Code Playgroud)
如果你在这里看到任何错误,请告诉我.