我需要调用Application.ExitThread()吗?

3
using System.Windows.Forms;

public class App
{
    [STAThread]
    public static void Main()
    {
        string fname;
        using (var d = new OpenFileDialog())
        {
            if (d.ShowDialog() != DialogResult.OK)
            {
                return;
            }
            fname = d.FileName;
        }
        //Application.ExitThread();
        for (; ;)
            ;
    }
}

上面的代码展示了一个文件对话框。一旦我选择了一个文件并按下“打开”按钮,for循环就会执行,但是(冻结的)对话框仍然存在。
一旦我取消注释Application.ExitThread(),对话框就会如预期般消失。
这是否达到了预期效果?为什么using不能让窗口消失?在哪里可以找到更多信息?

1
我已经测试了你的代码,(文件打开)对话框如预期地正确关闭。 - Jeroen van Langen
8
从那段代码无法重现你的问题。在 GUI 应用程序中,绝不能使用如 for(;;); 的死循环代码使主线程停止运行。许多功能将停止工作,尤其是绘画功能将不再发生。因此,窗口中的任何像素都无法更新,你可能会看到以前显示在窗口上方的内容的“幽灵”。投资一些时间学习一个好的教程或书籍,如何正确编写GUI代码通常不是通过试错而发现的。 - Hans Passant
2
把它视为诊断。你的代码不应该需要调用DoEvents()。 - H H
2
你应该在问题中提到这一点。无论如何,这并不改变问题。需要一个单线程公寓(STA)线程来拥有一个消息循环。对话框通过ShowDialog方法内部运行一个消息循环,只要它在屏幕上显示。但是一旦用户关闭它,消息循环就会消失。没有人再泵送消息,违反了STA线程的条件。实际上,你的应用程序已经变得愚蠢了,因为它被锁在一个紧密的无限循环中。 - Cody Gray
1
如果它开始了“另一个旅程的开始”,那么在操作系统级别上应该这样做:为什么不生成一个带有给定文件参数的控制台应用程序(隐藏窗口)?然后您的GUI应用程序可以优雅地终止。 - Daniel Gilbert
显示剩余5条评论
3个回答

12

你发现了单线程应用程序的主要问题......长时间运行的操作会冻结用户界面。

你的DoEvents()调用基本上“暂停”了你的代码,并给其他操作(如UI)运行的机会,然后恢复运行。问题在于,在再次调用DoEvents()之前,您的UI又被冻结了。实际上,DoEvents()是一个非常有问题的方法有些人称其为邪恶)。你真的不应该使用它。

你有更好的选择。

将长时间运行的操作放在另一个线程中,有助于确保UI保持响应并且你的工作效率最高。处理器能够在两个线程之间来回切换,给出同时执行的错觉,而不需要全面进行多进程操作。

其中一种更容易实现的方法是使用BackgroundWorker,尽管它们通常已经不再流行(出于本文不会涉及的原因:进一步阅读)。然而,它们仍然是.NET的一部分,相比其他方法学习曲线较低,因此我建议新开发人员在业余项目中尝试使用它们。

目前最佳的方法是使用.NET的Tasks库。如果你的长时间运行操作已经在一个线程中(例如,它是一个数据库查询,你只是等待它完成),并且如果库支持它,那么你可以利用async关键字来利用Tasks,而不必再考虑一次。即使它不在一个线程或者不在一个受支持的库中,你仍然可以启动一个新的Task,并通过Task.Run()在单独的线程中执行它。.NET Tasks具有内置的语言支持和更多的优势,比如协调多个Tasks和将Tasks链接在一起。

1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Cody Gray
1
@CodyGray - 我删除了关于DoEvents的部分,并链接到更多信息。老实说,我已经多年没有使用DoEvents了,而是更喜欢异步操作,并忘记了它实际上有多么棘手。 - JDB
@CodyGray - 尽管我仍然不同意你将“单线程应用程序”和“所有GUI工作都将在单个线程上完成”的混淆。在单线程应用程序中,所有工作,包括GUI和其他所有工作,都在单个线程上完成,这就是问题所在和重点 - OP需要一些后台线程来处理他/她的长时间运行的操作(这将使应用程序变为多线程)。 - JDB
我正在编写的应用程序(我的问题代码只是一个最小的示例)一旦我选择要打开的文件,就不再使用winforms。因此,它不会中断GUI的职责,而是“另一段旅程的开始”,也是winforms“刷新所有事物”的好时机。 - Alexander Aleksandrovič Klimov
2
@alex请阅读链接的材料(特别是“非常有问题的方法”),以便您知道自己正在做什么... DoEvents可能会产生非常严重的副作用。 - JDB

5

JDB已经在 他的答案 中解释了为什么(一般而言)您的代码不按预期工作。让我补充一点建议一个解决方法(针对您的特定情况,以及当您只需要使用系统对话框然后像控制台应用程序一样继续时)。

您正在尝试 使用 Application.DoEvents(),好吧,它似乎可以工作,在您的情况下,您没有可重入代码。但是,您确定所有相关消息都被正确处理了吗?您应该调用Application.DoEvents()多少次?您确定您正确地初始化了所有内容(我指的是ApplicationContext)吗?第二个问题更加实际,OpenFileDialog需要COM,COM(在这里)需要STAThreadSTAThread需要消息泵。我无法告诉您它将以何种方式失败,但肯定可能会失败。

首先请注意,通常应用程序使用 Application.Run() 启动主消息循环。你不会期望看到 new MyWindow().ShowDialog(),对吧?你的例子也不例外,让 Application.Run(Form) 重载为你创建 ApplicationContext(并在窗体关闭时处理 HandleDestroyed 事件,最终调用 - 惊喜 - Application.ExitThread())。不幸的是,OpenFileDialog 不从 Form 继承,因此你必须将其托管在一个 虚拟 窗体中以使用 Application.Run()

如果你在设计器中将对话框添加到窗体中,则不需要显式调用 dlg.Dispose()(让 WinForms 管理对象的生命周期)。

using System;
using System.Windows.Forms;

public class App
{
    [STAThread]
    public static void Main()
    {
        string fname = AskForFile();
        if (fname == null)
            return;

        LongRunningProcess(fname);
    }

    private static string AskForFile()
    {
        string fileName = null;

        var form = new Form() { Visible = false };
        form.Load += (o, e) => { 
            using (var dlg = new OpenFileDialog())
            {
                if (dlg.ShowDialog() == DialogResult.OK)
                    fileName = dlg.FileName;
            }

            ((Form)o).Close();
        };

        Application.Run(form);

        return fileName;
    }
}

OpenFileDialog似乎无法转换为ApplicationContext。 - Alexander Aleksandrovič Klimov
为什么你要这样做呢?如果你想模仿(出于学习目的,没有真正的理由去这样做)Application.Run()的功能,那么选择一个反汇编器或直接使用公共源代码。你会发现有比你预期的更多的代码。 - Adriano Repetti
我的意思是:似乎需要将OpenFileDialog强制转换为ApplicationContext才能运行您的代码 - 但这并不起作用。(我将您的代码复制并粘贴到我的IDE中,由于这个原因无法运行。) - Alexander Aleksandrovič Klimov
“你不需要显式调用dlg.Dispose()”这句话的上下文我不太确定,但我还是要挑刺一下。如果你正在模态显示对话框,那么你绝对需要调用Dispose方法。你在这里所做的,将其创建包装在using语句中,在道义上等同但更可取。 - Cody Gray
当然,你需要调用任何IDisposable成员的.Dispose()方法(并且using是等效的)。我的意思是(很抱歉没有表达清楚),当使用WinForms设计器将对话框插入窗体时,你不需要显式调用它。 - Adriano Repetti

0

不,您不必调用Application.ExitThread()

Application.ExitThread()终止调用线程的消息循环并强制销毁冻结对话框。虽然“那样可以”,但如果已知冻结的原因,则最好解除对话框的冻结。

在这种情况下,按下open似乎会触发一个关闭事件,该事件没有任何机会完成。 Application.DoEvents()给了它这个机会,并使对话框消失。


5
大多数情况下,“DoEvents”并不是一个好的建议。已有大量讨论讲述为什么“Sleep”(或者如问题中展示的忙等待)会导致UI冻结(例如http://stackoverflow.com/questions/27356165/why-does-thread-sleep-freeze-the-form),以及如何使用定时器、后台工作者或异步代码来正确地处理这些情况。 - Alexei Levenkov
11
小心,你正在从锅里跳到火炉里。你已经用DoEvents调用(可能更糟糕)替换了ExitThread(一个不好的想法)。这两种方法都是非常糟糕的hack,表明你不理解问题在哪里以及设计应用程序时正确的方式以避免该问题。由于你的问题完全没有描述你实际想要实现什么,因此我无法提供更多帮助。显然,没有一个真正的应用程序会使用无限的for (;;)循环。它将永远在GUI应用程序中失效。 - Cody Gray
我正在编写的应用程序(我的问题代码只是一个最小的示例)一旦我选择要打开的文件,就停止使用winforms。因此,它不是长时间运行中断GUI职责的事情 - 这是“另一段旅程的开始”,也是winforms“刷新所有事物”的好时机。 - Alexander Aleksandrovič Klimov

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