作成したコードにはいくつかの問題があります。私の意見では二つの主要な問題は、以下のとおりです。
- まず第一に、あなたが始めた、別々に実行しているプロセスで
BackgroundWorker
操作を混乱させているようです。両者は決して同じではなく、互いに関連しているものでもありません。 BackgroundWorker
をキャンセルしても、開始プロセスに直接影響はありません。あなたの質問は実際の希望行動がここに何であるかについてはっきりしていませんが、あなたは実際に外部プロセスを終了させるために何もしません。プロセスが出力を生成するのを待っているDoWork
メソッドをブロックしていない場合は、処理を中止することをお勧めします。そのまま処理を終了することなく、DoWork
は、ReadLine()
コールで停止しているため、キャンセルしようとしたことに気づくことはありません。
StandardOutput
ストリームとStandardError
ストリームを連続して、つまり1つずつ順番に消費しています。これは、コードをデッドロックするための非常に信頼できる方法であるため、ドキュメンテーションはこれに対して明確に警告しています。各ストリームのバッファは比較的小さく、バッファがいっぱいになったときにこれらのストリームの1つに書き込もうとすると、外部プロセス全体がハングします。これにより、ストリームのいずれにも出力が書き込まれなくなります。コード例でStandardOutput
ストリームを完全に読み取る前にStandardError
ストリームバッファがいっぱいになると、外部プロセスがハングアップし、独自のプロセスも同様に行われます。もう一つのマイナーな問題は、あなたが戻ってあなたがそれを追加できるUIスレッドにあなたは、出力とエラー文字列から読んだテキストを渡すために使用されている可能性がBackgroundWorker.ProgressChanged
イベント、を利用していないということです
テキストボックスにテキストを入力します。ここでControl.Invoke()
を使用することは厳密には必要ではなく、BackgroundWorker
の機能を十分に活用できません。
コードを変更して、BackgroundWorker
を使用して目標を達成できる方法があります。 1つの明らかな改善点は、オブジェクトフィールドProcess
をインスタンスフィールドに格納して、StopButton_Click()
メソッドでアクセスできるようにすることです。その方法では、Process.Kill()
メソッドを呼び出して、実際にプロセスを終了することができます。
しかし、そうであっても、あなたは今持っているデッドロックの起こりやすい実装を修正する必要があります。これはさまざまな方法で行うことができます:Process.OutputDataReceived
とProcess.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));
}
}
}
注:ここでは
はちょうどこのないあなたの例のバージョンである
- あなたが見ることができるように、まず、全く
BackgroundWorker
の必要性がなくなりました。 async
/await
パターンは暗黙のうちにBackgroundWorker
と同じ作業を行いますが、設定して管理するために必要な余分な定型コードはありません。
- 新しいインスタンスフィールド
_cancelTask
があります。これは、完了可能な単純なTask
オブジェクトを表しています。このケースではキャンセルされたことによって初めて完了しましたが、厳密には必須ではありませんし、タスク完了を監視するステートメントawait
は実際にタスクがどのように終了したか気にしません。それだけでした。より複雑なシナリオでは、実際には、Task
オブジェクトに対してResult
を使用し、SetResult()
を呼び出して値でタスクを完了し、SetCanceled()
を使用して、表現されている操作を実際にキャンセルしたい場合があります。それはすべて特定の文脈に依存します。
- メソッド(
Submit_Click()
メソッドと同じ)は、すべてが同期しているかのように記述されます。 await
ステートメントの "魔法"を通して、メソッドは実際には2つの部分で実行されます。ボタンをクリックすると、await
ステートメントまでのすべてのステートメントが実行されます。 await
で、RunProcess()
メソッドが戻ったら、button1_Click()
メソッドが返されます。 RunProcess()
によって返されたTask
オブジェクトが完了すると、後で実行を再開します。このメソッドは、そのメソッドが終了すると(つまり、最初に返されるときはではなくに戻ります)
button1_Click()
メソッドでは、現在の操作状態を反映するようにUIが更新されます。開始ボタンは無効になり、キャンセルボタンが有効になります。戻る前に、ボタンは元の状態に戻ります。
button1_Click()
メソッドは、_cancelTask
オブジェクトが作成され、後で破棄される方法です。 ステートメントは、RunProcess()
がスローした場合はTaskCanceledException
と表示されます。これは、操作がキャンセルされたことを報告したMessageBox
をユーザーに提示するために使用されます。あなたはもちろん、このような例外にも反応することができますが、あなたは合っています。
- このように、メソッドに相当する
button2_Click()
メソッドでは、_cancelTask
オブジェクトを完成状態(この場合はSetCanceled()
)に設定するだけで済みます。
RunProcess()
メソッドは、プロセスの主な処理が行われる方法です。プロセスを開始し、関連するタスクが完了するのを待ちます。出力ストリームとエラーストリームを表す2つのタスクは、Task.WhenAll()
の呼び出しで折り返されています。これにより、ラップされたすべてのタスクが完了したときにのみ完了する新しいTask
オブジェクトが作成されます。次に、メソッドは、そのラッパー・タスクのためにTask.WhenAny()
および_cancelTask
オブジェクトを介して待機します。 のいずれかが完了すると、メソッドは実行を完了します。完了したタスクが_cancelTask
オブジェクトであった場合、開始されたプロセスを強制終了し(実行していた途中で割り込みをかける)、プロセスが実際に終了するのを待って(これは、 wrapper task&hellip;これらは出力ストリームとエラーストリームの両方に到達したときに完了します。プロセスが終了したときに発生します)、次にTaskCanceledException
をスローします。
ConsumeReader()
メソッドは、与えられたTextReader
オブジェクトから単純にテキストを読み取り、出力をテキストボックスに追加するヘルパーメソッドです。それはTextReader.ReadAsync()
を使用します。このタイプのメソッドはTextReader.ReadLineAsync()
を使用して記述することもできますが、その場合は各行の最後に出力が表示されます。 ReadAsync()
を使用すると、改行文字を待たずに出力が利用可能になるとすぐに取得されます。
RunProcess()
とConsumeReader()
の方法もasync
であり、await
の文もあります。 button1_Click()
と同様に、これらのメソッドは、最初にawait
ステートメントに達したときに実行から復帰し、待たれたTask
が完了した後に実行を再開します。 ConsumeReader()
の例では、await
は、Result
のプロパティ値であるの値を、それが待機していたTask<int>
の値であることにも気付くでしょう。 await
ステートメントは、待機されたTask
のResult
の値に評価される式を形成します。
- これらの各ケースで
await
を使用することの非常に重要な特性は、UIスレッド上でメソッドの実行を再開することです。このため、button1_Click()
メソッドはawait
の後にbutton1
とbutton2
の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;
}
これは非同期でどのように私は見ていない:
は完全を期すため、ここでは上記
Form1
クラスと一緒に行くデザイナで生成されたコードです。これは単なるマルチスレッドです。[CancellationTokenSource](https://msdn.microsoft.com/en-us/library/system.threading.cancellationtokensource(v = vs.110).aspx)を参照してください。 –「CancelAsync()」コマンドを使用しないでください。このコマンドは現在の操作を停止しているようですが、その後は何もしません。 –
私はCancellationTokenSourceとCancellationTokenの使用を検討していると言っています。それはまさにそのために設計されたものです。 –