2016-08-09 5 views
1

저는 C#을 처음 접했고 배경 작업자를 이해하려고합니다.백그라운드 작업자 C#에서 프로세스로 실행되는 동안 cmd.exe 창을 닫는 방법?

현재이 코드를 실행하면 StopButton을 클릭 한 후 명령 프롬프트에서 출력을 읽거나 리디렉션하는 것을 중지하고 "취소됨"메시지를 남겨 둡니다. 그러나 나중에 아무 작업도 수행하지 않습니다. 현재이 모든 잘못된 구현할 수 있지만 e.Cancel CancellationPending = true로 CancelAsync(), 호출하는 중지 단추를 클릭하여 설정할 수 있습니다. 누구든지 내가 어떻게해야하는지에 대한 아이디어가 있습니까?

감사합니다. 도움을 주셔서 감사합니다.

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 메서드를 차단하지 않으면 프로세스를 포기합니다. 그대로, 프로세스를 종료하지 않고도 DoWorkReadLine() 호출에서 멈추었 기 때문에 사용자가 취소하려고 시도한 지점까지 절대로 도달 할 수 없습니다.
  2. StandardOutputStandardError 스트림을 순차적으로 사용하고 있습니다. 이 문서는 명확하게 경고합니다. 이는 코드를 교착 상태로 만드는 매우 신뢰할 수있는 방법이기 때문입니다. 각 스트림에 대한 버퍼는 상대적으로 작으며 버퍼가 가득 차면 이러한 스트림 중 하나에 쓰기를 시도하면 전체 외부 프로세스가 중지됩니다. 이것은 결과적으로 스트림에 더 이상의 출력이 기록되지 않게합니다. 코드 예에서 StandardOutput 스트림을 완전히 읽을 수 없으면 StandardError 스트림 버퍼가 가득 차면 외부 프로세스가 중지되고 사용자 프로세스가 처리됩니다.

또 다른 사소한 문제는 당신이 당신이를 추가 할 위치를 다시 UI 스레드로 출력 및 오류 문자열에서 읽은 텍스트를 전달하는 데 사용 수 있었던 BackgroundWorker.ProgressChanged 이벤트를 활용하지 않는다는 것입니다 텍스트를 텍스트 상자에 추가하십시오. 여기서 Control.Invoke()을 사용하는 것은 꼭 필요한 것은 아니며 BackgroundWorker의 기능을 완전히 활용하지 못했습니다.

작성한 코드를 수정하여 여전히 BackgroundWorker을 사용하고 목표를 달성 할 수있는 방법이 있습니다. 한 가지 분명한 개선 사항은 인스턴스 필드에 Process 개체 참조를 저장하여 StopButton_Click() 메서드로 액세스 할 수 있도록하는 것입니다. 이 방법에서는 Process.Kill() 메서드를 호출하여 프로세스를 실제로 종료 할 수 있습니다.

그래도 그렇다고해도 교착 상태가 발생하기 쉬운 구현을 수정해야합니다. 다양한 방법으로 수행 할 수 있습니다. Process.OutputDataReceivedProcess.ErrorDataReceived 이벤트를 사용하십시오. 스트림 중 하나를 처리하는 두 번째 BackgroundWorker 작업을 생성하십시오. 스트림을 읽으려면 Task 기반 이디엄을 사용하십시오.

내가 선호하는 마지막 옵션이 있습니다.이벤트 기반 패턴 (첫 번째 옵션)은 사용하기가 쉽지 않으며 (라인 기반이기 때문에 작업 과정에서 부분 라인을 작성하는 프로세스를 처리 할 때 제한된 값을 갖습니다) 두 번째 옵션은 불필요하게 장기 실행 스레드를 만듭니다.). 그러나 스트림을 읽기 위해 Task 기반의 관용구를 사용하려는 경우 전체 구현을 업그레이드해야한다고 생각합니다.

BackgroundWorker은 여전히 ​​하나가 원하는 경우에 사용 가능한 클래스이지만, async/await 키워드와 함께 새로운 Task 기능은 이럴 비동기 작업을 처리 할 수있는 훨씬 쉽고 청소기 방법입니다 무엇을 제공합니다. 가장 큰 장점 중 하나는 명시 적으로 사용되는 스레드 (예 : DoWork 이벤트 처리기를 스레드 풀 스레드에서 실행)에 의존하지 않는다는 것입니다. 시나리오 전체를 구성하는 비동기 입출력 작업은 API를 통해 암시 적으로 처리되므로 사용자가 작성한 모든 코드가 원하는 UI 스레드에서 실행될 수 있습니다. 당신이 볼 수있는 모든 BackgroundWorker 필요가 더 이상 존재,

  1. 첫째 :

    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)); 
         } 
        } 
    } 
    

    참고 : 여기에

    는이 작업을 수행하여 예를 들어 버전입니다. async/await 패턴은 암시 적으로 BackgroundWorker과 동일한 모든 작업을 수행하지만이를 설정하고 관리하는 데 필요한 추가 상용구 코드는 필요하지 않습니다.
  2. 새 인스턴스 필드 _cancelTask이 있으며 이는 완료 할 수있는 간단한 Task 개체를 나타냅니다. 취소 된 경우에만 완료되며이 경우 엄격하게 필수 및 필수 사항은 아니며 작업 완료를 모니터링하는 await 문은 실제로 작업이 어떻게 끝났는지 신경 쓰지 않습니다. 그냥 그렇게했습니다. 좀 더 복잡한 시나리오에서는 실제로 Task 객체에 Result을 사용하고 값이 지정된 작업을 완료하려면 SetResult()을 호출하고 실제로 표시되는 작업을 취소하려면 SetCanceled()을 사용하려고 할 수 있습니다. 그것은 모두 특정 상황에 달려 있습니다.
  3. 메서드 (사용자의 Submit_Click() 메서드와 동일)는 모든 것이 동시에 발생하는 것처럼 작성됩니다. await 구문의 "마법"을 통해 메서드는 실제로 두 부분으로 실행됩니다. 버튼을 클릭하면 await 문까지 실행됩니다. await에서 RunProcess() 메서드가 반환되면 button1_Click() 메서드가 반환됩니다. RunProcess()에 의해 반환 된 Task 개체가 완료 될 때 나중에 해당 메서드가 끝날 때 (즉, 이 아닌이 반환 될 때) 다시 실행됩니다.
  4. button1_Click() 메서드에서 UI는 현재 작업 상태를 반영하여 업데이트됩니다. 즉, 시작 단추가 비활성화되고 취소 단추가 사용됩니다. 돌아 오기 전에 버튼이 원래 상태로 돌아갑니다.
  5. button1_Click() 메서드는 _cancelTask 개체가 생성되어 나중에 삭제되는 메서드이기도합니다. await RunProcess() 문은 RunProcess()이 던져진 경우 TaskCanceledException이 표시됩니다. 작업이 취소되었음을보고하는 MessageBox을 사용자에게 표시하는 데 사용됩니다. 당신은 물론 당신이 적합하다고 생각하는 예외에 응답 할 수 있습니다.
  6. 이렇게하면 (StopButton_Click() 메서드에 해당하는) button2_Click() 메서드는 _cancelTask 개체를 완료 상태 (이 경우 SetCanceled())로 설정하면됩니다.
  7. RunProcess() 방법은 프로세스의 주요 처리가 발생하는 곳입니다. 프로세스를 시작한 다음 관련 작업이 완료 될 때까지 대기합니다. 출력 및 오류 스트림을 나타내는 두 개의 태스크는 Task.WhenAll() 호출에서 랩핑됩니다. 모든 랩된 타스크가 완료 될 때 완료 될 새 Task 오브젝트가 작성됩니다. 그런 다음 메서드는 해당 래퍼 작업 및 _cancelTask 개체에 대해 Task.WhenAny()을 통해 대기합니다. 중 하나가 완료되면 메서드가 실행을 완료합니다. 완료된 작업이 _cancelTask 개체 인 경우 메서드는 시작된 프로세스를 중단 (수행중인 작업의 중간에 중단)하고 프로세스가 실제로 종료 될 때까지 기다립니다 (프로세스가 완료 될 때까지 기다릴 수 있음). wrapper task & hellip; 이것은 출력과 오류 스트림의 양쪽 끝에 도달했을 때 완료되며, 프로세스가 종료 될 때 발생하고 TaskCanceledException을 던집니다.
  8. ConsumeReader() 메서드는 주어진 TextReader 개체에서 텍스트를 간단히 읽고 텍스트 상자에 출력을 추가하는 도우미 메서드입니다. 그것은 TextReader.ReadAsync()을 사용합니다; 이 유형의 메소드는 TextReader.ReadLineAsync()을 사용하여 작성할 수도 있지만이 경우 각 행의 끝 부분에만 출력이 표시됩니다. ReadAsync()을 사용하면 출력이 줄 바꿈 문자를 기다리지 않고 사용 가능한 즉시 검색됩니다.
  9. RunProcess()ConsumeReader() 메서드는 async이며 await 문도 포함되어 있습니다. button1_Click()과 마찬가지로이 메서드는 처음에 await 문에 도달하면 실행을 다시 시작하고 나중에 Task이 완료 될 때까지 실행을 다시 시작합니다. ConsumeReader() 예에서 은 Result 속성 값인 int 값의 압축을 푼다.이 값은 대기중인 Task<int>입니다. await 문은 대기 된 TaskResult 값으로 평가되는 식을 구성합니다.
  10. 이러한 경우 각각에서 await을 사용하면 매우 중요한 특징은 UI 스레드에서 메서드의 실행을 다시 시작한다는 것입니다. 따라서 button1_Click() 메서드는 await 뒤에 UI 개체 button1button2에 계속 액세스 할 수 있으며 ReadAsync() 메서드로 반환 될 때마다 개체가 textBox1 개체에 액세스하여 텍스트를 추가 할 수있는 이유는 여기에 있습니다.

나는 위의 내용을 많이 소화해야한다는 것을 알고 있습니다. 특히 그 대부분이 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()'을 호출하면 그 시점에서 프로세스가 종료됩니다. 나는. 효과적으로 당신이 원하는 것을하고 있습니다. 어떤 코드가 무엇을하는지 이해하는 것이 중요합니다. 외부 프로그램에 그렇게 할 수있는 방법이 있다고 가정 할 때 프로세스를 죽이는 것이 정상적으로 작동을 중단하는 것과는 다르다는 것에주의하십시오. 사물을 불완전한 상태로 남겨 둘 수 있습니다. –