创建一种从另一个线程安全访问控件的方法
我正在尝试编写一个“SafeInvoke”方法来处理尝试从另一个线程访问控件时可能发生的所有情况/问题。我已经看到了很多解决方案和很多与此相关的问题,虽然有些解决方案对于大多数人来说已经足够好了,但它们都没有考虑竞争条件(这意味着仍然有可能出现不需要的异常)。
这就是我到目前为止所拥有的,我尝试尽我所能地评论为什么我添加了一些 if 并尝试捕获。我还尝试仅捕获相关异常,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,它会再次变为 false 吗?
我为 IsHandleCreated 添加了 spinwait,因为我在控件的 OnLoad 事件中启动了 Task<>,并且任务可能在控件完全完成加载之前完成。但是,如果加载控件的时间超过 5 秒,我无论如何都会让任务完成,而不更新 GUI(否则我会有很多线程旋转等待可能不会再发生的事情)
如果您有任何优化建议或者发现任何可能仍会造成问题的错误或场景,请告诉我:)。
I am trying to write a 'SafeInvoke' method that handles all cases/problems that can occur when trying to access a control from another thread. I've seen a lot of solutions and a lot of questions about this and while there are some that are good enough for most people, they all fail to take race conditions in account (meaning that it's still possible to get an unwanted exception).
So this is what I have so far, I tried commenting as best as I could why I put some ifs and try catches. I also tried to catch only the relevant exceptions, InvalidOperationException is one that can occur for a wide range of reasons (including Collection was modified) and I didn't want to suppress those (because they have nothing to do with the safe invoking). To check that I based myself on the TargetSite.Name property of the exception, I also looked up the actual throw in reflector to see if there were any other locations that could cause an exception.
/// <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);
}
}
There is 1 thing I don't know for sure, which is if IsHandleCreated=true, will it ever become false again ?
I added the spinwait for IsHandleCreated because I started Task<>s in the OnLoad event of the control and it was possible that the task was finished before the control was completely finished with loading. If however it takes longer than 5sec to load a control I let the task finish anyway, without updating the GUI (otherwise I'd have a lot of threads spinwaiting for something that probably won't occur anymore)
If you have any suggestions for optimizations or find any bugs or scenario's that might still pose a problem, please let me know :).
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
老实说,当您在基本应用程序中使用 UI 线程访问控件时,您是否也会这样做进行检查?可能不会,您只是编码并期望控件存在并且不会被释放。为什么现在要进行这么多检查?
让多个线程访问 UI 不是一个好主意,但如果您没有其他方法,我建议您使用
Control.BeginInvoke
。使用Control.BeginInvoke
、Control.IsInvokeRequired
应该就足够了。实际上我从未使用过 Control.IsInvokeRequired,我事先就知道哪些访问将来自不同的线程,哪些不是。
Honestly, do you do so may checkings as well when you access a control/from with the UI thread in a basic application? Probably not, you just code and expect the control exists and is not disposed. Why do you that amount of checkings now?
It's not a good idea let multiple threads access the UI, but in case you have no other way I would recommend you to use
Control.BeginInvoke
. UsingControl.BeginInvoke
,Control.IsInvokeRequired
should be enough.Actually I've never used
Control.IsInvokeRequired
, I know before hand which access will come from a different thread and which no.