2016-08-09 7 views
1

私はC#の新機能で、バックグラウンドワーカーを理解しようとしています。バックグラウンドワーカーC#でプロセスとして実行されている間にcmd.exeウィンドウを閉じるには?

現在のところ、このコードを実行すると、StopButtonがクリックされた後、コマンドプロンプトからの出力のリダイレクトと読み込みが停止し、「キャンセル」メッセージが表示されたままになりますが、その後は何もしません。私は現在、これをすべて間違って実装しているかもしれませんが、CancelAsync()を呼び出すStopボタンをクリックすると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!"; 
     } 
    } 
+0

これは非同期でどのように私は見ていない:


は完全を期すため、ここでは上記Form1クラスと一緒に行くデザイナで生成されたコードです。これは単なるマルチスレッドです。[CancellationTokenSource](https://msdn.microsoft.com/en-us/library/system.threading.cancellationtokensource(v = vs.110).aspx)を参照してください。 –

+0

「CancelAsync()」コマンドを使用しないでください。このコマンドは現在の操作を停止しているようですが、その後は何もしません。 –

+0

私はCancellationTokenSourceとCancellationTokenの使用を検討していると言っています。それはまさにそのために設計されたものです。 –

答えて

0

作成したコードにはいくつかの問題があります。私の意見では二つの主要な問題は、以下のとおりです。

  1. まず第一に、あなたが始めた、別々に実行しているプロセスでBackgroundWorker操作を混乱させているようです。両者は決して同じではなく、互いに関連しているものでもありません。 BackgroundWorkerをキャンセルしても、開始プロセスに直接影響はありません。あなたの質問は実際の希望行動がここに何であるかについてはっきりしていませんが、あなたは実際に外部プロセスを終了させるために何もしません。プロセスが出力を生成するのを待っているDoWorkメソッドをブロックしていない場合は、処理を中止することをお勧めします。そのまま処理を終了することなく、DoWorkは、ReadLine()コールで停止しているため、キャンセルしようとしたことに気づくことはありません。
  2. StandardOutputストリームとStandardErrorストリームを連続して、つまり1つずつ順番に消費しています。これは、コードをデッドロックするための非常に信頼できる方法であるため、ドキュメンテーションはこれに対して明確に警告しています。各ストリームのバッファは比較的小さく、バッファがいっぱいになったときにこれらのストリームの1つに書き込もうとすると、外部プロセス全体がハングします。これにより、ストリームのいずれにも出力が書き込まれなくなります。コード例でStandardOutputストリームを完全に読み取る前にStandardErrorストリームバッファがいっぱいになると、外部プロセスがハングアップし、独自のプロセスも同様に行われます。もう一つのマイナーな問題は、あなたが戻ってあなたがそれを追加できるUIスレッドにあなたは、出力とエラー文字列から読んだテキストを渡すために使用されている可能性がBackgroundWorker.ProgressChangedイベント、を利用していないということです

テキストボックスにテキストを入力します。ここでControl.Invoke()を使用することは厳密には必要ではなく、BackgroundWorkerの機能を十分に活用できません。

コードを変更して、BackgroundWorkerを使用して目標を達成できる方法があります。 1つの明らかな改善点は、オブジェクトフィールドProcessをインスタンスフィールドに格納して、StopButton_Click()メソッドでアクセスできるようにすることです。その方法では、Process.Kill()メソッドを呼び出して、実際にプロセスを終了することができます。

しかし、そうであっても、あなたは今持っているデッドロックの起こりやすい実装を修正する必要があります。これはさまざまな方法で行うことができます:Process.OutputDataReceivedProcess.ErrorDataReceivedイベントを使用します。 1つのストリームを処理するために2番目のBackgroundWorkerタスクを作成します。ストリームを読むためにTaskベースのイディオムを使用してください。

私の好みが最後のオプションです。イベントベースのパターン(最初のオプション)は使用するのが面倒です(行ベースなので、操作の途中で部分行を書き込むプロセスを扱う際には値が限られています) )。しかし、ストリームを読み込むためにTaskベースのイディオムを使用する場合は、そうするために実装全体をアップグレードする必要があります。

BackgroundWorkerはまだ1がしたい場合に使用するための実行可能なクラスですが、async/awaitキーワードと一緒に新しいTask機能は私見非同期操作を処理するために、はるかに簡単かつクリーンな方法は何か提供します。最大の利点の1つは、明示的に使用されるスレッド(例えば、スレッドプールスレッドでDoWorkイベントハンドラを実行する)に依存しないことです。ここでのシナリオ全体を構成するような非同期入出力操作は、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. あなたが見ることができるように、まず、全くBackgroundWorkerの必要性がなくなりました。 async/awaitパターンは暗黙のうちにBackgroundWorkerと同じ作業を行いますが、設定して管理するために必要な余分な定型コードはありません。
  2. 新しいインスタンスフィールド_cancelTaskがあります。これは、完了可能な単純なTaskオブジェクトを表しています。このケースではキャンセルされたことによって初めて完了しましたが、厳密には必須ではありませんし、タスク完了を監視するステートメントawaitは実際にタスクがどのように終了したか気にしません。それだけでした。より複雑なシナリオでは、実際には、Taskオブジェクトに対してResultを使用し、SetResult()を呼び出して値でタスクを完了し、SetCanceled()を使用して、表現されている操作を実際にキャンセルしたい場合があります。それはすべて特定の文脈に依存します。
  3. メソッド(Submit_Click()メソッドと同じ)は、すべてが同期しているかのように記述されます。 awaitステートメントの "魔法"を通して、メソッドは実際には2つの部分で実行されます。ボタンをクリックすると、awaitステートメントまでのすべてのステートメントが実行されます。 awaitで、RunProcess()メソッドが戻ったら、button1_Click()メソッドが返されます。 RunProcess()によって返されたTaskオブジェクトが完了すると、後で実行を再開します。このメソッドは、そのメソッドが終了すると(つまり、最初に返されるときはではなくに戻ります)
  4. button1_Click()メソッドでは、現在の操作状態を反映するようにUIが更新されます。開始ボタンは無効になり、キャンセルボタンが有効になります。戻る前に、ボタンは元の状態に戻ります。
  5. button1_Click()メソッドは、_cancelTaskオブジェクトが作成され、後で破棄される方法です。 ステートメントは、RunProcess()がスローした場合はTaskCanceledExceptionと表示されます。これは、操作がキャンセルされたことを報告したMessageBoxをユーザーに提示するために使用されます。あなたはもちろん、このような例外にも反応することができますが、あなたは合っています。
  6. このように、メソッドに相当するbutton2_Click()メソッドでは、_cancelTaskオブジェクトを完成状態(この場合はSetCanceled())に設定するだけで済みます。
  7. RunProcess()メソッドは、プロセスの主な処理が行われる方法です。プロセスを開始し、関連するタスクが完了するのを待ちます。出力ストリームとエラーストリームを表す2つのタスクは、Task.WhenAll()の呼び出しで折り返されています。これにより、ラップされたすべてのタスクが完了したときにのみ完了する新しいTaskオブジェクトが作成されます。次に、メソッドは、そのラッパー・タスクのためにTask.WhenAny()および_cancelTaskオブジェクトを介して待機します。 のいずれかが完了すると、メソッドは実行を完了します。完了したタスクが_cancelTaskオブジェクトであった場合、開始されたプロセスを強制終了し(実行していた途中で割り込みをかける)、プロセスが実際に終了するのを待って(これは、 wrapper task&hellip;これらは出力ストリームとエラーストリームの両方に到達したときに完了します。プロセスが終了したときに発生します)、次にTaskCanceledExceptionをスローします。
  8. ConsumeReader()メソッドは、与えられたTextReaderオブジェクトから単純にテキストを読み取り、出力をテキストボックスに追加するヘル​​パーメソッドです。それはTextReader.ReadAsync()を使用します。このタイプのメソッドはTextReader.ReadLineAsync()を使用して記述することもできますが、その場合は各行の最後に出力が表示されます。 ReadAsync()を使用すると、改行文字を待たずに出力が利用可能になるとすぐに取得されます。
  9. RunProcess()ConsumeReader()の方法もasyncであり、awaitの文もあります。 button1_Click()と同様に、これらのメソッドは、最初にawaitステートメントに達したときに実行から復帰し、待たれたTaskが完了した後に実行を再開します。 ConsumeReader()の例では、awaitは、Resultのプロパティ値であるの値を、それが待機していたTask<int>の値であることにも気付くでしょう。 awaitステートメントは、待機されたTaskResultの値に評価される式を形成します。
  10. これらの各ケースでawaitを使用することの非常に重要な特性は、UIスレッド上でメソッドの実行を再開することです。このため、button1_Click()メソッドはawaitの後にbutton1button2のUIオブジェクトに引き続きアクセスでき、ReadAsync()メソッドによって返されるテキストがあるたびに、がtextBox1オブジェクトにアクセスしてテキストを追加できるのはなぜですか。

私は上記が消化することが多いかもしれないことを理解します。特に、私が最初に述べた主な2つの問題に取り組むのではなく、BackgroundWorkerからTaskベースのAPIへの完全な変更に関連する場合がほとんどです。しかし、これらの変更が暗黙的にどのように対処されているのか、現代のasync/awaitパターンを使用することで、コードの他の要件がより簡単で読みやすい方法でどのように満たされているかを確認できれば幸いです。

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; 
} 
+0

このような詳細な説明をいただきありがとうございます!私は自分のコードを書き直して、うまくいけば動作するコードを返すでしょう! –

+0

私はここで実行中にタスクをキャンセル扱うことができるフォローアップの質問があります:http://stackoverflow.com/questions/38903661/how-to-cancel-a-async-task-that-starts-a -process-in-c –

+0

@Ross: '_cancelTask​​.SetCanceled()'は外部プロセスをまったく停止しません。これは単にコードにシグナルを送るだけで、 'WhenAny()'の呼び出しを完了させることができます。これが起こると、 'Process.Kill()'の呼び出しは、その時点でプロセスを終了させます。私。効果的にあなたが望むことをやっています。コードのどの部分が何をしているのかを理解することは重要です。プロセスを強制終了することは、外部プログラムにもそれを行う方法があると仮定して、正常に操作を中断することと同じではないことに注意してください。物事を不完全な状態にする可能性があります。 –

関連する問題