【问题标题】:How to close cmd.exe window while its being ran as a process in a background worker C#?如何在后台工作程序 C# 中作为进程运行时关闭 cmd.exe 窗口?
【发布时间】:2016-08-09 23:50:21
【问题描述】:

我对 C# 非常陌生,并试图了解后台工作人员。

目前,当我运行此代码时,它会在单击 StopButton 后停止重定向并从命令提示符读取输出,并留下“已取消”消息,但之后什么也不做。我目前可能实现这一切都是错误的,但我通过单击调用 CancelAsync() 的停止按钮设置了 e.Cancel,这将 CancellationPending = true。有人对我应该如何处理有任何想法吗?

非常感谢!! 感谢您的帮助!

    public Form2()
    {
        InitializeComponent();

        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new   
        RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);

    }

    private void StopButton_Click(object sender, EventArgs e)
    {
        if (bw.WorkerSupportsCancellation == true)
        {
            bw.CancelAsync();
            bw.Dispose();

            Invoke(new ToDoDelegate(() => this.textBox2.Text += Environment.NewLine + "Cancelled " + Environment.NewLine));
        }
    }

    private void Submit_Click(object sender, EventArgs e)
    {
            if(bw.IsBusy != true)
            {
                bw.RunWorkerAsync();
                Invoke(new ToDoDelegate(() => this.textBox2.Text += "Starting Download " + Environment.NewLine));
            }

    }

    public bool DoSVNCheckout(String KeyUrl, DoWorkEventArgs e)
    {
        SVNProcess = new Process
        {
            StartInfo = new ProcessStartInfo
            {

                FileName = "cmd.exe",
                Arguments = "/C plink download files using svn"
                Verb = "runas",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = false,
            }
        };

        SVNProcess.Start();
        while(!SVNProcess.StandardOutput.EndOfStream & bw.CancellationPending == false)
        {
            string output = SVNProcess.StandardOutput.ReadLine();
            Invoke(new ToDoDelegate(() => this.textBox2.Text += output));
        }
        while (!SVNProcess.StandardError.EndOfStream & bw.CancellationPending == false)
        {
            string Erroutput = SVNProcess.StandardError.ReadLine();
            Invoke(new ToDoDelegate(() => this.textBox2.Text += Erroutput));
        }
        if(SVNProcess.StandardError.EndOfStream & bw.CancellationPending == false)
        {
            string Erroutput = SVNProcess.StandardError.ReadLine();
            Invoke(new ToDoDelegate(() => this.textBox2.Text += Erroutput));
        }

        //if I manually close the cmd.exe window by clicking X
        //in the top right corner the program runs correctly
        //at these lines of code right here

        if(bw.CancellationPending == true)
        {
            e.Cancel = true;
            return true;
        }
        return false;
    }

    private delegate void ToDoDelegate();
    private void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;


            if(bw.CancellationPending == true)
            {
                e.Cancel = true;
                return;
            }
            e.Cancel = DoSVNCheckout(URL, e);

    }
    private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {

        if ((e.Cancelled == true))
        {
            this.textBox2.Text = "Canceled!";
        }
        else{
            this.textBox2.Text = "Done!";
        }
    }

【问题讨论】:

  • 我看不出这是异步的。这只是多线程。看看CancellationTokenSource
  • 你是说我不应该使用命令“CancelAsync()”吗?该命令似乎确实停止了当前操作,但之后什么也不做。
  • 我是说考虑使用 CancellationTokenSource 和 CancellationToken。这正是它的设计目的。
  • @Felix:也许你可以详细说明。 IME,CancellationToken 很有用,如果有一个 CPU 绑定操作可以定期检查取消(例如调用ThrowIfCancellationRequested())以一种可靠的方式。但在这种情况下,当前进程只是在等待它启动的外部进程;恕我直言,设置某种循环只是为了轮询令牌似乎不如例如有用。创建一个TaskCompletionSource 来表示用户的异步输入(请参阅我提出的答案)。
  • @Felix:但是如果您知道更好的方法,可以更直接、更简洁地集成 CancellationToken,我鼓励您提供这些细节,因为它们会非常有用。

标签: c# asynchronous process backgroundworker


【解决方案1】:

您编写的代码存在许多问题。在我看来,两个主要问题是:

  1. 首先,您似乎将BackgroundWorker 操作与您已启动的单独运行的进程混淆了。两者绝不相同,甚至相互关联。取消BackgroundWorker 不会对您启动的进程产生任何直接影响。您的问题不清楚这里实际期望的行为是什么,但是您没有做任何事情来实际终止外部进程。充其量,如果您没有阻止 DoWork 方法等待进程产生输出,那么您将放弃该进程。事实上,如果不终止进程,您的 DoWork 永远不会注意到您试图取消它,因为它卡在 ReadLine() 调用上。
  2. 您正在以串行方式使用StandardOutputStandardError 流,即一个接一个。文档清楚地警告了这一点,因为这是使代码死锁的一种非常可靠的方法。每个流的缓冲区相对较小,如果在缓冲区已满时尝试写入其中一个流,则整个外部进程将挂起。反过来,这将导致不再将输出写入任何流。在您的代码示例中,如果在您能够完全读取 StandardOutput 流之前,StandardError 流缓冲区已满,则外部进程将挂起,您自己的进程也会挂起。

另一个小问题是您没有利用 BackgroundWorker.ProgressChanged 事件,您可以使用该事件将从输出中读取的文本和错误字符串传递回您可以添加该文本的 UI 线程到您的文本框。此处Control.Invoke() 的使用并非绝对必要,也无法充分利用BackgroundWorker 中的功能。

有一些方法可以修改您编写的代码,以便您仍然可以使用BackgroundWorker 并实现您的目标。您可以做出的一项明显改进是将Process 对象引用存储在实例字段中,以便StopButton_Click() 方法可以访问它。在该方法中,您可以调用Process.Kill() 方法来实际终止进程。

但即便如此,您仍需要修复现有的容易死锁的实现。这可以通过多种方式完成:使用Process.OutputDataReceivedProcess.ErrorDataReceived 事件;创建第二个BackgroundWorker 任务来处理其中一个流;使用基于Task 的惯用语来读取流。

最后一个选项是我的偏好。第二个选项不必要地创建了长时间运行的线程,而基于事件的模式(第一个选项)使用起来很尴尬(并且是基于行的,因此在处理在其操作过程中写入部分行的进程时价值有限)。但是,如果您打算使用基于 Task 的惯用语来读取流,在我看来您应该升级整个实现来这样做。

BackgroundWorker 仍然是一个可行的类,如果你愿意的话,但是新的Task 功能与async/await 关键字相结合,为恕我直言提供了一种更简单、更简洁的异步处理方式操作。最大的优点之一是它不依赖于显式使用的线程(例如,在线程池线程中运行 DoWork 事件处理程序)。异步 I/O 操作(例如构成此处整个场景的内容)通过 API 隐式处理,允许您编写的所有代码在您想要的 UI 线程中执行。

这是您的示例的一个版本,它就是这样做的:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private TaskCompletionSource<bool> _cancelTask;

    private async void button1_Click(object sender, EventArgs e)
    {
        button1.Enabled = false;
        button2.Enabled = true;
        _cancelTask = new TaskCompletionSource<bool>();

        try
        {
            await RunProcess();
        }
        catch (TaskCanceledException)
        {
            MessageBox.Show("The operation was cancelled");
        }
        finally
        {
            _cancelTask = null;
            button1.Enabled = true;
            button2.Enabled = false;
        }
    }

    private void button2_Click(object sender, EventArgs e)
    {
        _cancelTask.SetCanceled();
    }

    private async Task RunProcess()
    {
        Process process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "cmd.exe",
                Arguments = "/C pause",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = false,
            }
        };

        process.Start();

        Task readerTasks = Task.WhenAll(
            ConsumeReader(process.StandardError),
            ConsumeReader(process.StandardOutput));

        Task completedTask = await Task.WhenAny(readerTasks, _cancelTask.Task);

        if (completedTask == _cancelTask.Task)
        {
            process.Kill();
            await readerTasks;
            throw new TaskCanceledException(_cancelTask.Task);
        }
    }

    private async Task ConsumeReader(TextReader reader)
    {
        char[] text = new char[512];
        int cch;

        while ((cch = await reader.ReadAsync(text, 0, text.Length)) > 0)
        {
            textBox1.AppendText(new string(text, 0, cch));
        }
    }
}

注意事项:

  1. 首先,如您所见,根本不再需要BackgroundWorkerasync/await 模式隐含地完成了 BackgroundWorker 将为您完成的所有相同工作,但不需要设置和管理它所需的所有额外样板代码。
  2. 有一个新的实例字段_cancelTask,它代表一个可以完成的简单Task对象。在这种情况下,它只能通过取消来完成,但这并不是严格要求的……您会注意到监视任务完成的await 语句实际上并不关心任务是如何结束的。只是它做到了。在更复杂的场景中,人们可能实际上希望将Result 用于这样的Task 对象,调用SetResult() 以完成具有值的任务,并使用SetCanceled() 实际取消正在表示的操作。这一切都取决于具体的上下文。
  3. button1_Click() 方法(等同于您的Submit_Click() 方法)的编写方式好像一切都是同步发生的。通过await 语句的“魔力”,该方法实际上分两部分执行。当您单击按钮时,直到await 语句的所有内容都会执行;在await,一旦RunProcess() 方法返回,button1_Click() 方法就会返回。稍后它将在RunProcess() 返回的Task 对象完成时恢复执行,而这又会在该方法到达其结束时发生(即不是它第一次返回)。
  4. button1_Click() 方法中,更新了UI 以反映当前的操作状态:禁用开始按钮,启用取消按钮。在返回之前,按钮会返回到它们的原始状态。
  5. button1_Click() 方法也是创建 _cancelTask 对象并随后丢弃的地方。如果RunProcess() 抛出它,await RunProcess() 语句将看到TaskCanceledException;这用于向用户显示 MessageBox 报告操作已被取消。当然,您可以按照自己认为合适的方式响应此类异常。
  6. 这样,button2_Click() 方法(相当于您的StopButton_Click() 方法)只需将_cancelTask 对象设置为完成状态,在这种情况下通过调用SetCanceled()
  7. RunProcess() 方法是对进程进行主要处理的地方。它启动该过程,然后等待相关任务完成。代表输出流和错误流的两个任务封装在对Task.WhenAll() 的调用中。这将创建一个新的 Task 对象,该对象仅在完成所有包装任务后才会完成。然后该方法通过Task.WhenAny() 等待该包装任务和_cancelTask 对象。如果 其中一个 完成,则该方法将完成执行。如果完成的任务是_cancelTask 对象,则该方法通过终止已启动的进程(在它正在执行的任何操作中中断它)来执行此操作,等待进程实际退出(可以通过完成来观察包装器任务的......当输出和错误流都到达结束时这些完成,这反过来在进程退出时发生),然后抛出TaskCanceledException
  8. ConsumeReader() 方法是一个辅助方法,它简单地从给定的TextReader 对象中读取文本,并将输出附加到文本框中。它使用TextReader.ReadAsync();这种类型的方法也可以使用TextReader.ReadLineAsync() 编写,但在这种情况下,您只会在每行的末尾看到输出。使用 ReadAsync() 可确保在输出可用时立即检索输出,而无需等待换行符。
  9. 请注意RunProcess()ConsumeReader() 方法也是async,并且其中还包含await 语句。与button1_Click() 一样,这些方法最初在到达await 语句时从执行中返回,稍后在等待的Task 完成时恢复执行。在ConsumeReader() 示例中,您还会注意到await 解包int 值,它是它正在等待的Task&lt;int&gt;Result 属性值。 await 语句形成一个表达式,计算结果为等待的 TaskResult 值。
  10. 在每种情况下使用await 的一个非常重要的特性是框架会在UI 线程上恢复方法的执行。这就是为什么button1_Click()方法仍然可以访问await之后的UI对象button1button2,以及为什么ConsumeReader()可以访问textBox1对象,以便在每次返回一些时附加文本通过ReadAsync() 方法。

我意识到以上内容可能需要消化很多。尤其是当其中大部分涉及从使用BackgroundWorker 到基于Task 的API 的完全改变时,而不是解决我一开始提到的两个主要问题。但我希望您能看到这些更改是如何隐式解决这些问题的,以及如何使用现代async/await 模式以更简单、更易于阅读的方式满足代码的其他要求。


为了完整起见,下面是 Designer 生成的代码,与上述 Form1 类一起使用:

partial class Form1
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.button1 = new System.Windows.Forms.Button();
        this.button2 = new System.Windows.Forms.Button();
        this.textBox1 = new System.Windows.Forms.TextBox();
        this.SuspendLayout();
        // 
        // button1
        // 
        this.button1.Location = new System.Drawing.Point(12, 12);
        this.button1.Name = "button1";
        this.button1.Size = new System.Drawing.Size(75, 23);
        this.button1.TabIndex = 0;
        this.button1.Text = "Start";
        this.button1.UseVisualStyleBackColor = true;
        this.button1.Click += new System.EventHandler(this.button1_Click);
        // 
        // button2
        // 
        this.button2.Enabled = false;
        this.button2.Location = new System.Drawing.Point(93, 12);
        this.button2.Name = "button2";
        this.button2.Size = new System.Drawing.Size(75, 23);
        this.button2.TabIndex = 0;
        this.button2.Text = "Stop";
        this.button2.UseVisualStyleBackColor = true;
        this.button2.Click += new System.EventHandler(this.button2_Click);
        // 
        // textBox1
        // 
        this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
        | System.Windows.Forms.AnchorStyles.Left) 
        | System.Windows.Forms.AnchorStyles.Right)));
        this.textBox1.Location = new System.Drawing.Point(13, 42);
        this.textBox1.Multiline = true;
        this.textBox1.Name = "textBox1";
        this.textBox1.ReadOnly = true;
        this.textBox1.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
        this.textBox1.Size = new System.Drawing.Size(488, 258);
        this.textBox1.TabIndex = 1;
        // 
        // Form1
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(513, 312);
        this.Controls.Add(this.textBox1);
        this.Controls.Add(this.button2);
        this.Controls.Add(this.button1);
        this.Name = "Form1";
        this.Text = "Form1";
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.Button button1;
    private System.Windows.Forms.Button button2;
    private System.Windows.Forms.TextBox textBox1;
}

【讨论】:

  • 非常感谢您提供如此详细的解释!我将重新编写我的代码,并会回复一些有希望的工作代码!
  • 我有一个后续问题可以在这里查看,处理在运行时取消任务:stackoverflow.com/questions/38903661/…
  • @Ross: _cancelTask.SetCanceled() 根本不会直接停止外部进程。这只是对您的代码的一个信号,允许对WhenAny() 的调用完成。发生这种情况时,对Process.Kill() 的调用确实会在那时终止该进程。 IE。实际上它正在做你想做的事。了解代码的哪一部分在做什么很重要。请注意,终止进程与优雅地中断操作不同,假设外部程序甚至有办法做到这一点:您只是直接停止它;这可能会使事情处于不完整的状态。
猜你喜欢
  • 1970-01-01
  • 2011-01-13
  • 1970-01-01
  • 2016-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多