【问题标题】:Windows Form run external process without blocking UIWindows 窗体在不阻塞 UI 的情况下运行外部进程
【发布时间】:2014-09-10 23:29:20
【问题描述】:

我想:

  1. 显示带有文本框的表单。
  2. 运行外部程序(notepad.exe 以方便示例)。
  3. 在记事本运行时继续允许用户在表单文本框中输入数据。
  4. 在记事本关闭时运行更多(继续)本机表单代码。除其他外,这将更新表单。

我在实现这一点时遇到了问题。我知道很多关于这个类似问题的帖子,但还没有找到适合我的解决方案。

我试过了:

  • 执行 waitforexit,但这当然会阻塞 UI,用户无法输入数据。
  • 正在尝试异步进程调用,在该进程完成时调用另一个方法。这会导致从另一个线程调用新方法并且无法更新表单的问题。
  • 在 UI 中执行等待/睡眠循环,但这自然会阻塞 UI。

简单的 Windows 窗体程序最简洁、最简单的解决方案是什么?没有使用额外的类,所有代码都在Form1类中。

【问题讨论】:

标签: c# .net forms process


【解决方案1】:

当进程退出时,Process 类会触发 Exited 事件。您可以为该事件添加一个处理程序,以便在进程退出时执行代码而不阻塞 UI 线程:

process.EnableRaisingEvents = true;
process.Exited += (s, args) => DoStuff();

或者,您可以创建一个 Task 来表示该过程的完成,以利用 TPL 进行异步:

public static Task WhenExited(this Process process)
{
    var tcs = new TaskCompletionSource<bool>();
    process.EnableRaisingEvents = true;
    process.Exited += (s, args) => tcs.TrySetResult(true);
    return tcs.Task;
}

这将允许您编写:

await process.WhenExited();
UpdateUI();

【讨论】:

  • 最后,不要只是唠叨别人的想法……,一个体面的答案。
  • 第一段代码看起来最简单,但我在实现它时遇到了麻烦。特别是调用 DoStuff 方法(我已经创建)给了我“方法 'DoStuff' 没有重载需要 0 个参数”。使用 private void DoStuff(Object sender, System.EventArgs e) 调用方法
  • Process Exited 事件也将在工作线程上运行(生成的匿名方法),如果您尝试直接与 UI 线程交互将引发异常。
  • @MrBeatnik 然后要么传入这些参数,或者更明智的做法是进行无参数重载,因为我认为您不需要这些参数来做任何事情。
  • @lordjeb 正确,您需要编组到处理程序中的 UI 线程。另一种方法是让 TPL 为您处理它,如答案所示。
【解决方案2】:

给你:

    void Form1_Load(object sender, EventArgs e)
    {
        Task.Factory.StartNew(() =>
        {
            var p = Process.Start("notepad.exe");
            p.WaitForExit();
        }).ContinueWith(antecedant => { MessageBox.Show("Notepad closed"); });
    }

【讨论】:

  • 这是创建一个线程池线程,只是为了在进程运行时无所事事。最好用实际的异步庄园来写程序。
  • @Servy 它不是什么都不做。它正在等待外部进程完成。它在各个方面都是非阻塞的。
  • 参数 action 类型:System.Action 异步执行的动作委托。
  • 线程本身什么也没做。您将所有时间都花在安排它上,并且告诉它实际上不允许任何事情。如果没有实际上是异步的 解决方案,那么您就不得不忍受这种棘手的工作,但根本不是这样Process 类提供了一种实际上 异步方式,以便在完成时得到通知。代码没有阻塞 UI 线程,但它阻塞了线程池线程,这在某种程度上非常阻塞。
  • 您没有阻塞 UI 线程这一事实并不意味着您没有完全不必要地阻塞您正在分配的线程池线程。
【解决方案3】:

这是我最喜欢使用 BackgroundWorker 执行此类操作的方法。这样做的好处是 RunWorkerCompleted 回调位于主线程上,因此它可以与 UI 交互。

public partial class Form1 : Form
{
    ...
    private BackgroundWorker wrk;
    private void button1_Click(object sender, EventArgs e)
    {
        wrk = new BackgroundWorker();
        wrk.DoWork += (s, ea) => { /*Create your process and wait here*/ };
        wrk.RunWorkerCompleted += (s, ea) => { textBox1.Text = "Finished"; };
        wrk.RunWorkerAsync();
    }
}

【讨论】:

  • 这是创建一个新线程,以便在进程运行时它可以无所事事。
  • 您可能需要为此同步上下文...来自后台线程的 UI 更新通常会导致跨线程异常。
  • @Darek BGW 会自动执行此操作。这就是它存在的意义所在。
  • @Servy 从我在调试器中可以看出,这些方法中的任何一种:使用任务异步等待、使用 Process.Exited 事件、BackgroundWorker 等都使用额外的线程来处理异步情况。
  • @lordjeb 它将使用线程池线程几微秒。这与创建一个全新的完整线程、将其阻塞很长一段时间、然后只使用它来完成相同的几微秒的实际工作完全不同。 TPL 方法将通过使用 TPL 避免创建硬线程,并且它只会将时间消耗几乎无法估量的时间,而不是几分钟。这是一个非常显着的差异。
【解决方案4】:

您应该在 BackgroundWorker 中启动进程,这样您就可以在同一线程上捕获完成事件。

        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += delegate {
            Process proc = Process.Start("YOUR-PROCESS-PATH");
            proc.Start();
            proc.WaitForExit();
        }
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;
        worker.RunWorkerAsync();

然后在被调用线程上捕获工人结束事件;

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //Do your thing o UI thread
    }

【讨论】:

  • 这是创建一个新线程,以便在进程运行时它可以无所事事。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多