BackgroundWorkerを生成するフォームがあり、フォームの(メインスレッド上の)テキストボックスを更新する必要があるため、Invoke((Action) (...));
呼び出しを行います。HandleClosingEvent
でbgWorker.CancelAsync()
を実行すると、Invoke(...)
呼び出しでObjectDisposedException
を取得します。しかし、HandleClosingEvent
に座ってbgWorkerが完了するのを待つと、.Invoke(...)が返ってこないことも理解できます。
例外やデッドロックを取得せずにこのアプリを閉じる方法はありますか?
以下は、単純なForm1クラスの3つの関連メソッドです。
public Form1() {
InitializeComponent();
Closing += HandleClosingEvent;
this.bgWorker.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
while (!this.bgWorker.CancellationPending) {
Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToString(); }));
}
}
private void HandleClosingEvent(object sender, CancelEventArgs e) {
this.bgWorker.CancelAsync();
/////// while (this.bgWorker.CancellationPending) {} // deadlock
}
私が知っているこれを行う唯一のデッドロックセーフおよび例外セーフな方法は、実際にFormClosingイベントをキャンセルすることです。 BGWがまだ実行されている場合はe.Cancel = trueに設定し、ユーザーがクローズを要求したことを示すフラグを設定します。次に、BGWのRunWorkerCompletedイベントハンドラーでそのフラグを確認し、設定されている場合はClose()を呼び出します。
private bool closePending;
protected override void OnFormClosing(FormClosingEventArgs e) {
if (backgroundWorker1.IsBusy) {
closePending = true;
backgroundWorker1.CancelAsync();
e.Cancel = true;
this.Enabled = false; // or this.Hide()
return;
}
base.OnFormClosing(e);
}
void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
if (closePending) this.Close();
closePending = false;
// etc...
}
別の方法を見つけました。 backgroundWorkersがもっとある場合は、以下を作成できます。
List<Thread> bgWorkersThreads = new List<Thread>();
そして、すべてのbackgroundWorkerのDoWorkメソッドで次を行います。
bgWorkesThreads.Add(Thread.CurrentThread);
使用できるArter:
foreach (Thread thread in this.bgWorkersThreads)
{
thread.Abort();
}
これをControlのWordアドインで使用しました。これはCustomTaskPane
で使用します。誰かが以前にドキュメントまたはアプリケーションを閉じると、すべてのbackgroundWorkesが作業を終了し、いくつかのCOM Exception
(どちらが正確に覚えていないか)が発生します。CancelAsync()
は機能しません。
しかし、これにより、backgroundworkers
で使用されるすべてのスレッドをDocumentBeforeClose
イベントですぐに閉じることができ、問題は解決します。
ここに私の解決策がありました(VB.Netにあります)。
FormClosingイベントを実行するとき、BackgroundWorker1.CancelAsync()を実行してCancellationPending値をTrueに設定します。残念ながら、プログラムは実際に値CancellationPendingの値を確認してe.Cancelをtrueに設定する機会を得ることができません(私が知る限り、BackgroundWorker1_DoWorkでのみ実行できます)。私はその行を削除しませんでしたが、実際には違いを生むようには見えません。
グローバル変数bClosingFormをTrueに設定する行を追加しました。次に、終了ステップを実行する前に、BackgroundWorker_WorkCompletedにコード行を追加して、e.Cancelledとグローバル変数bClosingFormの両方をチェックしました。
このテンプレートを使用すると、バックグラウンドワーカーが何かの途中にいる場合でも、いつでもフォームを閉じることができるはずです(これは良くないかもしれませんが、処理される可能性があるため、発生するはずです)。必要かどうかはわかりませんが、これがすべて行われた後、Form_Closedイベントに完全にバックグラウンドワーカーを配置できます。
Private bClosingForm As Boolean = False
Private Sub SomeFormName_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
bClosingForm = True
BackgroundWorker1.CancelAsync()
End Sub
Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
'Run background tasks:
If BackgroundWorker1.CancellationPending Then
e.Cancel = True
Else
'Background work here
End If
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
If Not bClosingForm Then
If Not e.Cancelled Then
'Completion Work here
End If
End If
End Sub
フォームのデストラクタでシグナルを待つことはできませんか?
AutoResetEvent workerDone = new AutoResetEvent();
private void HandleClosingEvent(object sender, CancelEventArgs e)
{
this.bgWorker.CancelAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
while (!this.bgWorker.CancellationPending) {
Invoke((Action) (() => { this.textBox1.Text =
Environment.TickCount.ToString(); }));
}
}
private ~Form1()
{
workerDone.WaitOne();
}
void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e )
{
workerDone.Set();
}
まず、ObjectDisposedExceptionは、ここで考えられる落とし穴の1つにすぎません。 OPのコードを実行すると、多くの場合に次のInvalidOperationExceptionが生成されます。
ウィンドウハンドルが作成されるまで、InvokeまたはBeginInvokeをコントロールで呼び出すことはできません。
コンストラクターではなく「Loaded」コールバックでワーカーを開始することでこれを修正できると思いますが、BackgroundWorkerのProgressレポートメカニズムが使用される場合、この試練全体を完全に回避できます。以下はうまく機能します:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
while (!this.bgWorker.CancellationPending)
{
this.bgWorker.ReportProgress(Environment.TickCount);
Thread.Sleep(1);
}
}
private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.textBox1.Text = e.ProgressPercentage.ToString();
}
パーセンテージパラメーターをハイジャックしましたが、他のオーバーロードを使用してパラメーターを渡すことができます。
上記のスリープコールを削除するとUIが詰まり、CPUが大量に消費され、メモリ使用量が継続的に増加することに注意してください。オーバーロードされているGUIのメッセージキューと関係があると思います。ただし、スリープコールがそのままの状態では、CPU使用率は事実上0であり、メモリ使用率も問題ないようです。賢明であるためには、おそらく1ミリ秒よりも高い値を使用する必要がありますか?ここに専門家の意見をいただければ幸いです...Update:更新があまり頻繁でない限り、問題ないようです:- リンク
いずれにせよ、GUIの更新が数ミリ秒より短い間隔で行われなければならないシナリオ(少なくとも、人間がGUIを見ているシナリオ)は予測できないので、ほとんどの場合、進捗報告は正しい選択です
バックグラウンドワーカーは、Invokeを使用してテキストボックスを更新しないでください。 UIスレッドに、イベントProgressChangedを使用して、添付されたテキストボックスに入れる値を指定してテキストボックスを更新するように適切に要求する必要があります。
イベントClosed(またはイベントClosing)の間、UIスレッドは、バックグラウンドワーカーをキャンセルする前にフォームが閉じられたことを記憶します。
ProgressChangedを受け取ると、UIスレッドはフォームが閉じられているかどうかを確認し、閉じられていない場合にのみ、テキストボックスを更新します。
This.enabled = falseを使用している場合、この場合DoEventsがこのような悪い選択と見なされる理由は本当にわかりません。私はそれがかなりきれいになると思います。
protected override void OnFormClosing(FormClosingEventArgs e) {
this.Enabled = false; // or this.Hide()
e.Cancel = true;
backgroundWorker1.CancelAsync();
while (backgroundWorker1.IsBusy) {
Application.DoEvents();
}
e.cancel = false;
base.OnFormClosing(e);
}
これはすべての人には機能しませんが、1秒ごとまたは10秒ごとなど、BackgroundWorkerで何かを定期的に実行している場合(おそらくサーバーをポーリングする)、これは正常にプロセスを停止し、エラーメッセージなしでプロセスを停止するようです(少なくともこれまで)、簡単にフォローできます。
public void StopPoll()
{
MyBackgroundWorker.CancelAsync(); //Cancel background worker
AutoResetEvent1.Set(); //Release delay so cancellation occurs soon
}
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
while (!MyBackgroundWorker.CancellationPending)
{
//Do some background stuff
MyBackgroundWorker.ReportProgress(0, (object)SomeData);
AutoResetEvent1.WaitOne(10000);
}
}