C#Winforms线程:调用Closed Form

时间:2010-08-25 12:01:00

标签: c# winforms thread-safety invoke

以下代码说明了我的困境。代码创建一个后台线程来处理某些东西,然后用结果调用UI线程。

如果后台线程在表单关闭后调用窗体上的Invoke,则可能会抛出异常。它在调用Invoke之前检查IsHandleCreated,但是在检查后表单可能会关闭。

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();
}

一种解决方案可能是同步FormClosing和每次调用Invoke,但这听起来不是很优雅。有更简单的方法吗?

4 个答案:

答案 0 :(得分:5)

是的,这里有一场比赛。在目标开始运行之前,A需要很长的时间。如果使用Control.BeginInvoke(),它将更好地工作,表单的Dispose()实现将清空调度队列。但这仍然是一场比赛,尽管很少会发生。您在代码段中编写的代码不需要Invoke()。

唯一干净的解决方法是将FormClosing事件和延迟关闭联锁,直到您确认后台线程已完成且无法再次启动。您的代码不容易,因为这需要“完成”回调,因此您可以真正关闭表单。 BackgroundWorker would be a better mousetrap。 Q& D修复是捕获BeginInvoke将引发的ObjectDisposedException。鉴于使用BeginInvoke()时这种情况有多么罕见,这种丑陋的黑客行为是可以接受的。你无法测试它:)

答案 1 :(得分:2)

我通过使用Hans Passant建议来捕获ObjectDisposedException来解决BeginInvoke的这个同步问题。到目前为止,它似乎有效。我创建了Control类的扩展方法来实现这一点。

TryBeginInvoke尝试在控件上调用自己的方法。如果成功调用该方法,它将检查控件是否已被释放。如果已经处理,它会立即返回;否则,它将最初作为参数传递给TryBeginInvoke的方法调用。代码如下:

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);
}

答案 2 :(得分:1)

看看WindowsFormsSynchronizationContextPost方法在UI线程上调用您的UpdateUI委托,而无需专用窗口;这样您就可以跳过调用IsHandleCreatedInvoke

修改:MSDN在"Multithreaded Programming with the Event-based Asynchronous Pattern"下有一些代码示例。

您可能会发现通过位于AsyncOperationManager之上的WindowsFormsSynchronizationContext类进行编程更加容易。反过来,BackgroundWorker组件构建在AsyncOperationManager之上。

UI线程被定义为您调用AsyncOperationManager.CreateOperation的线程;你想在CreateOperation开始时调用MyMethod,当你知道你在UI线程上时,并在局部变量中捕获它的返回值。

答案 3 :(得分:0)

您可以在调用之前检查表单(或任何控件)上的IsDisposed。

如果表格在此期间被处理,你还应该在你正在调用的实际方法中检查这个。