views:

230

answers:

4

The following code demonstrates my dilemma. The code creates a background thread which processes something, then Invokes the UI thread with the result.

It may throw an exception if the background thread calls Invoke on the form after the form has closed. It checks IsHandleCreated before calling Invoke, but the form might close after the check.

void MyMethod()
{
    // Define background thread
    Action action = new Action(
        () =>
        {
            // Process something
            var data = BackgroundProcess();

            // Try to ensure the form still exists and hope
            // that doesn't change before Invoke is called
            if (!IsHandleCreated)
                return;

            // Send data to UI thread for processing
            Invoke(new MethodInvoker(
                () =>
                {
                    UpdateUI(data);
                }));
        });

    // Queue background thread for execution
    action.BeginInvoke();
}

One solution might be to synchronize FormClosing and every call to Invoke, but that doesn't sound very elegant. Is there an easier way?

A: 

You can check IsDisposed on the form (or any control) before Invoking on it.

You should also check this inside of the actual method you're Invoking, in case the form was disposed in the meantime.

Bob
The problem is, in some places Invoke is throwing ObjectDisposedException even though it checks IsDisposed immediately before the call.
drifter
+1  A: 

Take a look at WindowsFormsSynchronizationContext. The Post method posts call to your UpdateUI delegate on the UI thread without needing a dedicated window; this lets you skip calling IsHandleCreated and Invoke.

Edit: MSDN has some code examples under "Multithreaded Programming with the Event-based Asynchronous Pattern".

You might find it easier to program via the AsyncOperationManager class, which sits on top of WindowsFormsSynchronizationContext. In turn, the BackgroundWorker component is built on top of AsyncOperationManager.

The UI thread is defined as the one on which you call AsyncOperationManager.CreateOperation; you want to call CreateOperation at the start of MyMethod, when you know you're on the UI thread, and capture its return value in a local variable.

Tim Robinson
I like that a lot. Some of my users are stubborn to update to .NET 4, but a case is mounting in its favor.
drifter
That's a .NET 2.0 class. Doesn't solve the problem, Control.Invoke already uses it. Post() will throw ObjectDisposedException.
Hans Passant
But `WindowsFormsSynchronizationContext` posts via a hidden control that's closed after your form is closed, and just before the program exits. Your form isn't involved at all.
Tim Robinson
I am trying to test this, but the documentation seems limited. I'm not sure how to specify the UI thread as the target of the Post method.
drifter
I tried it and what I am seeing is that `Post` and `Send` both deadlock when the form is closed between the check of `IsHandleCreated` and `Post`/`Send`.
Brian Gideon
These objects apparently expand the options available to me in synchronization. Beyond me for this quick fix, but it will surely help in the future. Thumb up.
drifter
@drifter I can recommend it. I don't get affected by these `IsHandleCreated`/disposed issues any more.
Tim Robinson
+2  A: 

Yes, there's a race here. A takes a good millisecond before the target starts running. It will work 'better' if you use Control.BeginInvoke() instead, the form's Dispose() implementation will empty the dispatch queue. But that's still a race, albeit that it will strike very rarely. Your code as written in the snippet doesn't require Invoke().

The only clean fix is to interlock the FormClosing event and to delay the close until you got confirmation that the background thread is completed and can't be started again. Not easy to do with your code as is since that requires a 'completed' callback so you can really get the form closed. BackgroundWorker would be a better mousetrap. The Q&D fix is to catch the ObjectDisposedException that BeginInvoke will raise. Given how rare this will be when you use BeginInvoke(), that ugly hack could be palpable. You just can't test it :)

Hans Passant
That may at least mitigate it. My code as written doesn't require Invoke? These exceptions are thrown about 1 out of 10 times. I can test them! :)
drifter
It doesn't, no need to delay the thread and wait for the delegate to finish running. You're not doing anything *after* the Invoke call. You can't really test it once you use BeginInvoke, you'll need to open and close your form at least a million times. Rest assured that the race is still there, you'll need to catch ODE.
Hans Passant
I wouldn't mind catching the ObjectDisposedException, but sometimes it throws InvalidOperationException instead ("Invoke or BeginInvoke cannot be called on a control until the window handle has been created.") I can't catch that unless I can differentiate it from other InvalidOperationException's in the invoked code, right?
drifter
Are you sure you don't call MyMethod too early? It won't be safe to call until the Load event has run.
Hans Passant
Positive. The exception only occurs when the form closes.
drifter
Double checking for you, I can only reproduce the IOE when I open and close the form very fast.
drifter
Well, follow the link I gave you. That's the right way to do it.
Hans Passant
Thanks, I'll try that next time.
drifter
A: 

I solved this synchronization issue for BeginInvoke by using Hans Passant's recommendation to catch the ObjectDisposedException. So far, it appears to work. I created extension methods of the Control class to facilitate this.

TryBeginInvoke attempts to invoke its own method on the control. If the method is successfully invoked, it checks whether the control has been disposed. If it has been disposed, it returns immediately; otherwise, it calls the method originally passed as a parameter to TryBeginInvoke. The code is as follows:

public static class ControlExtension
{
    // --- Static Fields ---
    static bool _fieldsInitialized = false;
    static InvokeDelegateDelegate _methodInvokeDelegate;  // Initialized lazily to reduce application startup overhead [see method: InitStaticFields]
    static InvokeMethodDelegate _methodInvokeMethod;  // Initialized lazily to reduce application startup overhead [see method: InitStaticFields]

    // --- Public Static Methods ---
    public static bool TryBeginInvoke(this Control control, Delegate method, params object[] args)
    {
        IAsyncResult asyncResult;
        return TryBeginInvoke(control, method, out asyncResult, args);
    }

    /// <remarks>May return true even if the target of the invocation cannot execute due to being disposed during invocation.</remarks>
    public static bool TryBeginInvoke(this Control control, Delegate method, out IAsyncResult asyncResult, params object[] args)
    {
        if (!_fieldsInitialized)
            InitStaticFields();

        asyncResult = null;

        if (!control.IsHandleCreated || control.IsDisposed)
            return false;

        try
        {
            control.BeginInvoke(_methodInvokeDelegate, control, method, args);
        }
        catch (ObjectDisposedException)
        {
            return false;
        }
        catch (InvalidOperationException)  // Handle not created
        {
            return false;
        }

        return true;
    }

    public static bool TryBeginInvoke(this Control control, MethodInvoker method)
    {
        IAsyncResult asyncResult;
        return TryBeginInvoke(control, method, out asyncResult);
    }

    /// <remarks>May return true even if the target of the invocation cannot execute due to being disposed during invocation.</remarks>
    public static bool TryBeginInvoke(this Control control, MethodInvoker method, out IAsyncResult asyncResult)
    {
        if (!_fieldsInitialized)
            InitStaticFields();

        asyncResult = null;

        if (!control.IsHandleCreated || control.IsDisposed)
            return false;

        try
        {
            control.BeginInvoke(_methodInvokeMethod, control, method);
        }
        catch (ObjectDisposedException)
        {
            return false;
        }
        catch (InvalidOperationException)  // Handle not created
        {
            return false;
        }

        return true;
    }

    // --- Private Static Methods ---
    private static void InitStaticFields()
    {
        _methodInvokeDelegate = new InvokeDelegateDelegate(InvokeDelegate);
        _methodInvokeMethod = new InvokeMethodDelegate(InvokeMethod);
    }
    private static object InvokeDelegate(Control control, Delegate method, object[] args)
    {
        if (!control.IsHandleCreated || control.IsDisposed)
            return null;

        return method.DynamicInvoke(args);
    }
    private static void InvokeMethod(Control control, MethodInvoker method)
    {
        if (!control.IsHandleCreated || control.IsDisposed)
            return;

        method();
    }

    // --- Private Nested Types ---
    delegate object InvokeDelegateDelegate(Control control, Delegate method, object[] args);
    delegate void InvokeMethodDelegate(Control control, MethodInvoker method);
}
drifter