创建一种从另一个线程安全访问控件的方法

发布于 2024-10-30 17:16:58 字数 5959 浏览 1 评论 0原文

我正在尝试编写一个“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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

花之痕靓丽 2024-11-06 17:16:58

老实说,当您在基本应用程序中使用 UI 线程访问控件时,您是否也会这样做进行检查?可能不会,您只是编码并期望控件存在并且不会被释放。为什么现在要进行这么多检查?

让多个线程访问 UI 不是一个好主意,但如果您没有其他方法,我建议您使用 Control.BeginInvoke。使用 Control.BeginInvokeControl.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. Using Control.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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文