C# Winforms多线程:关闭的窗体被调用

5
以下代码展示了我的困境。该代码创建了一个后台线程,处理某些内容,然后使用结果调用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个回答

5

是的,这里存在一种竞争情况。在目标开始运行之前,A需要花费好几毫秒的时间。如果你使用Control.BeginInvoke(),就会“更好”,因为表单的Dispose()实现将清空调度队列。但这仍然是一种竞赛,尽管它很少发生。在代码片段中,你不需要Invoke()。

唯一的解决方法是对FormClosing事件进行交错,并且要“延迟”关闭,直到你确认后台线程已经完成并且不能再次启动。由于你的代码需要一个“完成”回调,所以这个操作并不容易。使用BackgroundWorker可能会更好。Q&D(快速而粗糙)的解决方案是捕获BeginInvoke()会引发的ObjectDisposedException异常。鉴于当使用BeginInvoke()时,这种情况非常罕见,这种丑陋的hack可能是可以接受的。只是你无法测试它 :)


这至少可以缓解一下。按照我的代码编写方式,不需要调用Invoke?这些异常大约有10分之1的概率被抛出。我可以测试它们! :) - drifter
不需要延迟线程并等待委托完成,因为没有在Invoke调用之后执行其他操作。一旦使用BeginInvoke,您就无法真正测试它,需要至少打开和关闭窗体1000000次。请放心,竞争仍然存在,您需要捕获ODE。 - Hans Passant
我不介意捕获ObjectDisposedException,但有时它会抛出InvalidOperationException(“在窗口句柄创建之前,无法在控件上调用Invoke或BeginInvoke。”)。除非我能将其与调用代码中的其他InvalidOperationException区分开来,否则我无法捕获它,对吧? - drifter
你确定你不会过早调用MyMethod吗?在Load事件运行之前调用将不安全。 - Hans Passant
正常情况下不会出现异常,只有在窗体关闭时才会发生。 - drifter
显示剩余2条评论

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

1

看一下WindowsFormsSynchronizationContextPost方法在UI线程上发布对UpdateUI委托的调用,而不需要专用窗口;这使您可以跳过调用IsHandleCreatedInvoke

编辑:MSDN在"基于事件的异步模式的多线程编程"下提供了一些代码示例。

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

UI线程被定义为您在其中调用AsyncOperationManager.CreateOperation的线程;您希望在MyMethod开始时调用CreateOperation,当您知道自己在UI线程上,并将其返回值捕获在本地变量中。


我非常喜欢那个。我的一些用户很顽固,不愿更新到.NET 4,但越来越多的理由支持更新。 - drifter
那是一个.NET 2.0类。它并不能解决问题,Control.Invoke已经在使用它了。Post()会抛出ObjectDisposedException异常。 - Hans Passant
但是 WindowsFormsSynchronizationContext 通过一个隐藏的控件进行发送,该控件在您的窗体关闭之后且程序退出之前被关闭。您的窗体根本不涉及到此过程。 - Tim Robinson
这些对象显然扩展了我在同步方面可用的选项。对于这个快速修复超出了我的能力,但它肯定会在未来有所帮助。点赞。 - drifter
@drifter 我可以推荐它。我不再受到这些 IsHandleCreated/disposed 问题的影响。 - Tim Robinson
显示剩余2条评论

0

在对窗体(或任何控件)进行调用之前,您可以检查其上的IsDisposed属性。

此外,在实际调用方法内部也应该检查这一点,以防在此期间窗体被释放。


1
问题在于,有些地方即使在调用之前检查了IsDisposed,Invoke仍会抛出ObjectDisposedException异常。 - drifter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接