我对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!";
}
}
答案 0 :(得分:0)
您编写的代码存在许多问题。在我看来,两个主要问题是:
BackgroundWorker
操作与您已启动的单独运行进程混淆。这两者绝不是相同的,甚至是相互关联的。取消BackgroundWorker
取消对您开始的流程有任何直接影响。您的问题并不清楚这里的实际所需行为是什么,但您没有做任何事情来实际终止外部流程。最好的情况是,如果您没有阻止DoWork
方法等待流程产生输出,那么您就放弃了这个过程。事实上,如果不终止此过程,您的DoWork
就永远无法达到通知您已尝试取消它的程度,因为它会卡在ReadLine()
电话上。 StandardOutput
和StandardError
个流,即一个接一个地使用。文档明确警告不要这样做,因为这是一种非常可靠的方法来解决代码问题。每个流的缓冲区相对较小,如果缓冲区已满时尝试写入其中一个流,则整个外部进程将挂起。这将导致不再将输出写入任何流。在您的代码示例中,如果StandardError
流缓冲区在您能够完全读取StandardOutput
流之前已满,则外部进程将挂起,您自己的进程也会挂起。另一个小问题是您没有利用BackgroundWorker.ProgressChanged
事件,您可以使用该事件将您从输出和错误字符串中读取的文本传递回UI线程您可以在其中将该文本添加到文本框中。此处Control.Invoke()
的使用并非绝对必要,也无法充分利用BackgroundWorker
中的功能。
有一些方法可以修改您编写的代码,这样您仍然可以使用BackgroundWorker
来实现目标。您可以做出的一个明显改进是将Process
对象引用存储在实例字段中,以便StopButton_Click()
方法可以访问它。在该方法中,您可以调用Process.Kill()
方法来实际终止该过程。
但即便如此,您仍需要解决现在已经遇到的死锁问题。这可以通过多种方式完成:使用Process.OutputDataReceived
和Process.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));
}
}
}
注意:
BackgroundWorker
。 async
/ await
模式隐含地完成了BackgroundWorker
对您的所有相同工作,但没有设置和管理它所需的所有额外样板代码。_cancelTask
,它表示可以完成的简单Task
对象。在这种情况下,它只是被取消而完成,但并非严格要求......您会注意到监控任务完成的await
语句不会被取消实际上关心任务是如何结束的。就是这样。在更复杂的场景中,实际上可能希望将Result
用于此类Task
对象,调用SetResult()
以使用值完成任务,并实际使用SetCanceled()
取消正在表示的操作。这一切都取决于具体的背景。button1_Click()
方法(相当于您的Submit_Click()
方法)被写成好像一切都是同步发生的。通过&#34;魔术&#34;在await
语句中,该方法实际上分两部分执行。单击按钮时,执行await
语句之前的所有内容;在await
,RunProcess()
方法返回后,button1_Click()
方法返回。当Task
返回的RunProcess()
对象完成时,它将在稍后恢复执行,当该方法到达终点时(即不是第一次返回时),它将发生button1_Click()
方法中,更新UI以反映当前的操作状态:禁用启动按钮并启用取消按钮。在返回之前,按钮将返回其原始状态。button1_Click()
方法也是_cancelTask
对象的创建位置,后来被丢弃。如果await RunProcess()
抛出TaskCanceledException
,RunProcess()
语句会看到MessageBox
;这用于向用户显示操作已被取消的button2_Click()
报告。你当然可以回应这样的例外,但是你觉得合适。StopButton_Click()
方法(相当于您的_cancelTask
方法)只需要将SetCanceled()
对象设置为已完成状态,在这种情况下,通过调用RunProcess()
}。Task.WhenAll()
方法是进程的主要处理方式。它启动该过程,然后等待相关任务完成。表示输出和错误流的两个任务包含在对Task
的调用中。这将创建一个新的Task.WhenAny()
对象,该对象仅在完成所有包装任务时才会完成。然后,该方法通过_cancelTask
等待该包装器任务和_cancelTask
对象。如果 完成,该方法将完成执行。如果已完成的任务是TaskCanceledException
对象,则该方法通过终止已启动的进程(在其正在执行的任务中间中断它)来等待进程实际退出(可以通过包装器任务的完成......当达到输出和错误流的结束时,这些完成,当进程退出时反过来),然后抛出ConsumeReader()
。TextReader
方法是一种帮助方法,它简单地从给定的TextReader.ReadAsync()
对象中读取文本,并将输出附加到文本框。它使用TextReader.ReadLineAsync()
;这种类型的方法也可以使用ReadAsync()
编写,但在这种情况下,您只能看到每行末尾的输出。使用RunProcess()
可确保输出在可用时立即检索,而无需等待换行符。ConsumeReader()
和async
方法也是await
,其中也包含button1_Click()
个语句。与await
一样,这些方法最初在到达Task
语句时从执行返回,稍后在等待ConsumeReader()
完成时继续执行。在await
示例中,您还会注意到int
解包Result
值,即Task<int>
正在等待的await
属性值上。 Task
语句形成一个表达式,用于评估等待的Result
await
值。button1_Click()
的一个非常重要的特性是框架恢复在UI线程上执行方法 。这就是为什么button1
方法仍然可以在button2
之后访问UI对象await
和ConsumeReader()
,以及为什么textBox1
可以访问ReadAsync()
每次在BackgroundWorker
方法返回一些文本时附加文本的对象。我意识到以上可能需要消化很多。特别是当它的大部分内容与使用Task
到基于async
的API的完全变化有关时,而不是解决我在开始时提到的两个主要问题。但我希望你能看到这些变化是如何隐含地解决这些问题的,以及如何通过使用现代await
/ {{1}以更简单,更易于阅读的方式满足代码的其他要求模式。
为了完整起见,这里是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;
}