问题
I am very new to C# and trying to understand background workers.
Currently when I run this code it will stop redirecting and reading the output from the command prompt after the StopButton has been clicked and leaves the "cancelled" message behind, but then does nothing afterwards. I currently may be implementing this all wrong, but I have the e.Cancel set by clicking the stop button that calls CancelAsync(), which turns CancellationPending = true. Anyone have any ideas how I should go about this?
Thanks so much!! I appreciate the help!
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!";
}
}
回答1:
There are a number of problems with the code you've written. The two primary issues, in my opinion are:
- First and foremost, you seem to be confusing the
BackgroundWorkeroperation with the separately-running process you've started. The two are not by any means the same, or even related to each other. Cancelling theBackgroundWorkerwon't have any direct effect on the process you start. Your question is not clear on what the actual desired behavior is here, but you don't do anything to actually terminate the external process. At best, if you weren't blocking theDoWorkmethod waiting for the process to produce output, you'd abandon the process. As it is, without terminating the process, yourDoWorkcan never get to a point where it notices you've tried to cancel it, because it's stuck on aReadLine()call. - You are consuming the
StandardOutputandStandardErrorstreams in serial fashion, i.e. one after the other. The documentation clearly warns against this, as this is a very reliable way to deadlock your code. The buffers for each stream are relatively small, and the entire external process will hang if it tries to write to one of these streams when the buffer is full. This will in turn cause no more output to be written to any of the streams. In your code example, if theStandardErrorstream buffer gets full before you have been able to completely read theStandardOutputstream, the external process will hang and your own process will as well.
Another minor issue is that you don't take advantage of the BackgroundWorker.ProgressChanged event, which you could have used to pass text you've read from the output and error strings back to the UI thread where you could add that text to your text box. The use of Control.Invoke() here isn't strictly necessary, and fails to fully take advantage of the features in BackgroundWorker.
There are ways to modify the code you've written, so that you can still use BackgroundWorker and achieve your goal. One obvious improvement that you could make would be to store the Process object reference in an instance field, so that it's accessible by the StopButton_Click() method. In that method, you could call the Process.Kill() method to actually terminate the process.
But even so, you would need to fix the deadlock-prone implementation you've got now. This can be done in a variety of ways: use the Process.OutputDataReceived and Process.ErrorDataReceived events; create a second BackgroundWorker task to process one of the streams; use the Task-based idioms to read the streams.
The last option there would be my preference. The second option needlessly creates long-running threads, while the event-based pattern (the first option) is awkward to use (and is line-based, so has limited value when dealing with processes that write partial lines during the course of their operation). But if you're going to use the Task-based idioms for reading the streams, it seems to me you should upgrade the entire implementation to do so.
BackgroundWorker is still a viable class to use if one wants to, but the new Task features in conjunction with the async/await keywords offer what is IMHO a much easier and cleaner way to handle asynchronous operations. One of the biggest advantages is that it doesn't rely on explicitly used threads (e.g. running the DoWork event handler in a thread pool thread). Asynchronous I/O operations such as what makes up the entirety of your scenario here, are handled implicitly via the API, allowing all of the code you write to be executed in the UI thread where you want it.
Here is a version of your example that does just this:
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));
}
}
}
Notes:
- First, as you can see, there is no longer a need for
BackgroundWorkerat all. Theasync/awaitpattern implicitly does all of the same work thatBackgroundWorkerwould have for you, but without all the extra boilerplate code required to set it up and manage it. - There is a new instance field,
_cancelTask, which represents a simpleTaskobject that can be completed. It's only ever completed by being cancelled, in this case, but that's not strictly required…you'll note that theawaitstatement that monitors the task completion doesn't actually care how the task ended. Just that it did. In more complex scenarios, one might actually want to use theResultfor such aTaskobject, callingSetResult()to complete the task with a value, and usingSetCanceled()for actually cancelling the operation being represented. It all depends on the specific context. - The
button1_Click()method (equivalent to yourSubmit_Click()method) is written as if everything happens synchronously. Through the "magic" of theawaitstatement, the method actually executes in two parts. When you click the button, everything up to theawaitstatement executes; at theawait, once theRunProcess()method has returned, thebutton1_Click()method returns. It will resume execution later, when theTaskobject returned byRunProcess()completes, which in turn will happen when that method reaches its end (i.e. not the first time it returns). - In the
button1_Click()method, the UI is updated to reflect the current state of operation: the start button is disabled and the cancel button is enabled. Before returning, the buttons are returned to their original state. - The
button1_Click()method is also where the_cancelTaskobject is created and later discarded. Theawait RunProcess()statement will see aTaskCanceledExceptionif theRunProcess()throws it; this is used to present the user with theMessageBoxreporting that the operation had been cancelled. You can of course respond to an exception like this however you see fit. - In this way, the
button2_Click()method (equivalent to yourStopButton_Click()method) only has to set the_cancelTaskobject to a completed state, in this case by callingSetCanceled(). - The
RunProcess()method is where the main handling of the process occurs. It starts the process, then waits for the relevant tasks to complete. The two tasks representing the output and error streams are wrapping in a call toTask.WhenAll(). This creates a newTaskobject that will complete only when all of the wrapped tasks are completed. Then the method waits viaTask.WhenAny()for that wrapper task and the_cancelTaskobject. If either completes, the method will complete execution. If the completed task was the_cancelTaskobject, the method does so by killing the process that was started (interrupting it in the middle of whatever it was doing), waiting for the processs to actually exit (which can be observed by the completion of the wrapper task…these complete when the end of both the output and the error streams is reached, which in turn happens when the process has exited), and then throwing theTaskCanceledException. - The
ConsumeReader()method is a helper method that simple reads text from the givenTextReaderobject, and appends the output to the text box. It usesTextReader.ReadAsync(); this type of method can also be written usingTextReader.ReadLineAsync(), but in that case you will only see output at the end of each line. UsingReadAsync()ensures that output is retrieved as soon as it's available, without waiting for newline characters. - Note that the
RunProcess()andConsumeReader()methods are alsoasync, and also haveawaitstatements in them. As withbutton1_Click(), these methods initially return from executing upon reaching theawaitstatement, resuming execution later when the awaitedTaskcompletes. In theConsumeReader()example, you'll also notice thatawaitunpacks theintvalue that is theResultproperty value of theTask<int>it was waiting on. Theawaitstatement forms an expression that evaluates to the waitedTask'sResultvalue. - A very important characteristic of the use of
awaitin each of these cases is that the framework resumes execution of the method on the UI thread. This is why thebutton1_Click()method can still access the UI objectsbutton1andbutton2after theawait, and whyConsumeReader()can access thetextBox1object for the purpose of appending text each time some is returned by theReadAsync()method.
I realize the above might be a lot to digest. Especially when most of it relates to the complete change from the use of BackgroundWorker to the Task-based API, rather than addressing the main two issues I mentioned at the outset. But I hope you can see how those issues are implicitly addressed by these changes, as well as how other requirements of the code are met in a simpler, easier-to-read fashion by using the modern async/await pattern.
For completeness, here is the Designer-generated code that goes along with the above Form1 class:
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;
}
来源:https://stackoverflow.com/questions/38862178/how-to-close-cmd-exe-window-while-its-being-ran-as-a-process-in-a-background-wor