我正在尝试编写一个'SafeInvoke'方法来处理尝试从另一个线程访问控件时可能发生的所有情况/问题。我已经看到了很多关于这方面的解决方案和很多问题,虽然有一些对大多数人来说已经足够好了,但他们都没有考虑到竞争条件(意味着它仍然可能得到一个不需要的例外)。
所以这就是我到目前为止所做的一切,我尽可能地尽力评论为什么我会放一些ifs并尝试捕获。我也试图只捕获相关的异常,InvalidOperationException是一个可以出现的原因很多(包括Collection被修改),我不想压制那些(因为它们与安全调用无关)。为了检查我是否基于异常的TargetSite.Name属性,我还查找了实际的反射器,以查看是否有任何其他位置可能导致异常。
/// <summary>
/// Safely invokes an action on the thread the control was created on (if accessed from a different thread)
/// </summary>
/// <typeparam name="T">The return type</typeparam>
/// <param name="c">The control that needs to be invoked</param>
/// <param name="a">The delegate to execute</param>
/// <param name="spinwaitUntilHandleIsCreated">Waits (max 5sec) until the the control's handle is created</param>
/// <returns>The result of the given delegate if succeeded, default(T) if failed</returns>
public static T SafeInvoke<T>(this Control c, Func<T> a, bool spinwaitUntilHandleIsCreated = false)
{
if (c.Disposing || c.IsDisposed) // preliminary dispose check, not thread safe!
return default(T);
if (spinwaitUntilHandleIsCreated) // spin wait until c.IsHandleCreated is true
{
if (!c.SpinWaitUntilHandleIsCreated(5000)) // wait 5sec at most, to prevent deadlock
return default(T);
}
if (c.InvokeRequired) // on different thread, need to invoke (can return false if handle is not created)
{
try
{
return (T)c.Invoke(new Func<T>(() =>
{
// check again if the control is not dispoded and handle is created
// this is executed on the thread the control was created on, so the control can't be disposed
// while executing a()
if (!c.Disposing && !c.IsDisposed && c.IsHandleCreated)
return a();
else // the control has been disposed between the time the other thread has invoked this delegate
return default(T);
}));
}
catch (ObjectDisposedException ex)
{
// sadly the this entire method is not thread safe, so it's still possible to get objectdisposed exceptions because the thread
// passed the disposing check, but got disposed afterwards.
return default(T);
}
catch (InvalidOperationException ex)
{
if (ex.TargetSite.Name == "MarshaledInvoke")
{
// exception that the invoke failed because the handle was not created, surpress exception & return default
// this is the MarhsaledInvoke method body part that could cause this exception:
// if (!this.IsHandleCreated)
// {
// throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));
// }
// (disassembled with reflector)
return default(T);
}
else // something else caused the invalid operation (like collection modified, etc.)
throw;
}
}
else
{
// no need to invoke (meaning this is *probably* the same thread, but it's also possible that the handle was not created)
// InvokeRequired has the following code part:
// Control wrapper = this.FindMarshalingControl();
// if (!wrapper.IsHandleCreated)
// {
// return false;
// }
// where findMarshalingControl goes up the parent tree to look for a parent where the parent's handle is created
// if no parent found with IsHandleCreated, the control itself will return, meaning wrapper == this and thus returns false
if (c.IsHandleCreated)
{
try
{
// this will still yield an exception when the IsHandleCreated becomes false after the if check (race condition)
return a();
}
catch (InvalidOperationException ex)
{
if (ex.TargetSite.Name == "get_Handle")
{
// it's possible to get a cross threadexception
// "Cross-thread operation not valid: Control '...' accessed from a thread other than the thread it was created on."
// because:
// - InvokeRequired returned false because IsHandleCreated was false
// - IsHandleCreated became true just after entering the else bloc
// - InvokeRequired is now true (because this can be a different thread than the control was made on)
// - Executing the action will now throw an InvalidOperation
// this is the code part of Handle that will throw the exception
//
//if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
//{
// throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
//}
//
// (disassembled with reflector)
return default(T);
}
else // something else caused the invalid operation (like collection modified, etc.)
throw;
}
}
else // the control's handle is not created, return default
return default(T);
}
}
有一件事我不确定,如果IsHandleCreated = true,它会再次变为假吗?
我为IsHandleCreated添加了spinwait,因为我在控件的OnLoad事件中启动了Task&lt;&gt; s,并且在控件完全加载完成之前,任务可能已完成。然而,如果加载一个控件花费的时间超过5秒,我就让任务完成,而不更新GUI(否则我会有很多线程等待可能不会再发生的事情)
如果您有任何优化建议或发现任何可能仍然存在问题的错误或方案,请告诉我们:)。
答案 0 :(得分:0)
老实说,当您在基本应用程序中使用UI线程访问控件时,您是否可以进行检查?可能不是,你只是编码并期望控制存在并且不被处置。为什么现在有那么多的检查?
让多个线程访问用户界面并不是一个好主意,但如果您没有其他方式,我建议您使用Control.BeginInvoke
。使用Control.BeginInvoke
,Control.IsInvokeRequired
就足够了。
实际上我从未使用Control.IsInvokeRequired
,我事先知道哪个访问权限来自不同的帖子,哪个没有。