开发者

Creating a way to safely access controls from another thread

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 diff开发者_开发百科erent 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 :).


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜