C# Winforms 线程:调用封闭表单

发布于 2024-09-15 14:10:55 字数 877 浏览 2 评论 0原文

下面的代码演示了我的困境。该代码创建一个后台线程来处理某些内容,然后使用结果调用 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,但这听起来不太优雅。有更简单的方法吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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

评论(4

一直在等你来 2024-09-22 14:10:56

我通过使用 Hans Passant 的建议捕获 ObjectDisposeException 解决了 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);
}

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);
}
橘虞初梦 2024-09-22 14:10:56

看一下 WindowsFormsSynchronizationContext Post 方法将调用发布到 UI 线程上的 UpdateUI 委托,而无需专用窗口;这可以让您跳过调用 IsHandleCreatedInvoke

编辑: MSDN 在"多线程编程下有一些代码示例使用基于事件的异步模式”

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

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

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.

寒冷纷飞旳雪 2024-09-22 14:10:56

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

您还应该在您调用的实际方法中检查这一点,以防表单同时被处理。

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.

寒冷纷飞旳雪 2024-09-22 14:10:55

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

唯一干净的修复方法是互锁 FormClosing 事件并延迟关闭,直到您确认后台线程已完成且无法再次启动。按原样处理您的代码并不容易,因为这需要“完成”回调,以便您可以真正关闭表单。 BackgroundWorker 会是一个更好的捕鼠器。 Q&D 修复是捕获 BeginInvoke 将引发的 ObjectDisposeException。考虑到当您使用 BeginInvoke() 时这种情况很少见,这种丑陋的黑客行为是可以接受的。你只是无法测试它:)

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 acceptable. You just can't test it :)

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