ワーカースレッドに次のコードがあります(以下のImageListView
はControl
から派生しています):
if (mImageListView != null &&
mImageListView.IsHandleCreated &&
!mImageListView.IsDisposed)
{
if (mImageListView.InvokeRequired)
mImageListView.Invoke(
new RefreshDelegateInternal(mImageListView.RefreshInternal));
else
mImageListView.RefreshInternal();
}
ただし、上記のObjectDisposedException
メソッドでInvoke
を取得することがあります。コントロールは、IsDisposed
をチェックしてからInvoke
を呼び出すまでの間に破棄できるようです。どうすればそれを回避できますか?
コードには暗黙の競合状態があります。コントロールは、IsDisposedテストとInvokeRequiredテストの間に配置できます。 InvokeRequiredとInvoke()の間にもう1つあります。コントロールがスレッドの寿命を超えていることを確認せずにこれを修正することはできません。スレッドがリストビューのデータを生成していることを考えると、リストビューが消える前にスレッドの実行を停止する必要があります。
これを行うには、FormClosingイベントでe.Cancelを設定し、ManualResetEventで停止するようにスレッドに通知します。スレッドが完了したら、Form.Close()を再度呼び出します。 BackgroundWorkerを使用すると、スレッド完了ロジックを簡単に実装できます。サンプルコードは この投稿 にあります。
ここにあるのは 競合状態 です。 ObjectDisposed例外をキャッチして、それで済ませたほうがよいでしょう。実際、この場合、それはonlyの実用的なソリューションだと思います。
try
{
if (mImageListView.InvokeRequired)
mImageListView.Invoke(new YourDelegate(thisMethod));
else
mImageListView.RefreshInternal();
}
catch (ObjectDisposedException ex)
{
// Do something clever
}
現実には、Invokeとその仲間では、破棄されたコンポーネントでの呼び出し、またはハンドルがないためにInvalidOperationExceptionが発生するのを完全に防ぐことはできません。先制テストやロックセマンティクスを使用して完全に解決することはできない、実際の基本的な問題に対処するスレッドのいずれにおいても、以下のような答えはまだ実際には見ていません。
これが通常の「正しい」イディオムです。
// the event handler. in this case preped for cross thread calls
void OnEventMyUpdate(object sender, MyUpdateEventArgs e)
{
if (!this.IsHandleCreated) return; // ignore events if we arn't ready, and for
// invoke if cant listen to msg queue anyway
if (InvokeRequired)
Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData);
else
this.MyUpdate(e.MyData);
}
// the update function
void MyUpdate(Object myData)
{
...
}
基本的な問題:
Invoke機能を使用する場合、Windowsメッセージキューが使用されます。このキューは、メッセージをキューに入れて、メッセージの投稿または送信とまったく同じように、クロススレッド呼び出しを待機または起動して忘れます。 Invokeメッセージの前に、コンポーネントとそのウィンドウハンドルを無効にするメッセージがある場合、または実行しようとしたチェックの直後に配置されたメッセージがある場合は、悪い時間を過ごすことになります。
x thread -> PostMessage(WM_CLOSE); // put 'WM_CLOSE' in queue
y thread -> this.IsHandleCreated // yes we have a valid handle
y thread -> this.Invoke(); // put 'Invoke' in queue
ui thread -> this.Destroy(); // Close processed, handle gone
y thread -> throw Invalid....() // 'Send' comes back, thrown on calling thread y
コントロールがキューからそれ自体を削除しようとしていることを知る実際の方法はなく、呼び出しを「元に戻す」ために実行できる本当に合理的な方法はありません。何回チェックしたり、追加のロックを行ったりしても、他の誰かがクローズなどを発行するのを止めたり、非アクティブ化したりすることはできません。これが発生する可能性のあるシナリオはたくさんあります。
解決策:
最初に気付くのは、(IsHandleCreated)チェックがイベントを無視する方法と同じように、呼び出しが失敗することです。目標が非UIスレッドで呼び出し元を保護することである場合は、例外を処理し、成功しなかった他の呼び出しと同じように扱う必要があります(アプリがクラッシュしたり、何かをしたりしないようにするためです。リロール呼び出し機能、キャッチは知る唯一の方法です。
// the event handler. in this case preped for cross thread calls
void OnEventMyWhatever(object sender, MyUpdateEventArgs e)
{
if (!this.IsHandleCreated) return;
if (InvokeRequired)
{
try
{
Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData);
}
catch (InvalidOperationException ex) // pump died before we were processed
{
if (this.IsHandleCreated) throw; // not the droids we are looking for
}
}
else
{
this.MyUpdate(e.MyData);
}
}
// the update function
void MyUpdate(Object myData)
{
...
}
例外フィルタリングは、ニーズに合わせて調整できます。ほとんどのアプリケーションでは、ワーカースレッドがUIスレッドの処理とロギングのすべてを備えていないことが多いため、ワーカー側で例外を処理することをお勧めします。または、それらすべてをログに記録して再スローします。多くの場合、ワーカースレッドでキャッチされない例外は、アプリがクラッシュすることを意味します。
使ってみてください
if(!myControl.Disposing)
; // invoke here
私はあなたとまったく同じ問題を抱えていました。コントロールの.Disposedのチェックに切り替えてから、ObjectDisposedExceptionはなくなりました。これで100%修正されるとは言わず、99%だけです;)Disposedのチェックと呼び出しの呼び出しの間に競合状態が発生する可能性はありますが、実行したテストでは実行していません。その中に(私はThreadPoolとワーカースレッドを使用します)。
呼び出す各呼び出しの前に使用するものは次のとおりです。
private bool IsControlValid(Control myControl)
{
if (myControl == null) return false;
if (myControl.IsDisposed) return false;
if (myControl.Disposing) return false;
if (!myControl.IsHandleCreated) return false;
if (AbortThread) return false; // the signal to the thread to stop processing
return true;
}
BackGroundWorkerが可能である場合、これを回避する非常に 単純な 方法があります:
public partial class MyForm : Form
{
private void InvokeViaBgw(Action action)
{
BGW.ReportProgress(0, action);
}
private void BGW_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (this.IsDisposed) return; //You are on the UI thread now, so no race condition
var action = (Action)e.UserState;
action();
}
private private void BGW_DoWork(object sender, DoWorkEventArgs e)
{
//Sample usage:
this.InvokeViaBgw(() => MyTextBox.Text = "Foo");
}
}
lock(mImageListView){...}の可能性がありますか?
この質問も参照してください。
クロススレッドWinFormイベント処理でのInvoke/BeginInvokeの問題を回避しますか?
結果として得られたユーティリティクラス EventHandlerForControl は、イベントメソッドシグネチャのこの問題を解決できます。このクラスを適応させるか、その中のロジックを確認して問題を解決することができます。
ここでの本当の問題は、winformsでのクロススレッド呼び出しに指定されたAPIは本質的にスレッドセーフではないと指摘しているため、nobugzが正しいことです。 InvokeRequiredおよびInvoke/BeginInvoke自体の呼び出し内でも、予期しない動作を引き起こす可能性のあるいくつかの競合状態があります。
ミューテックスを使用できます。
スレッドの開始時のどこか:
Mutex m=new Mutex();
次に:
if (mImageListView != null &&
mImageListView.IsHandleCreated &&
!mImageListView.IsDisposed)
{
m.WaitOne();
if (mImageListView.InvokeRequired)
mImageListView.Invoke(
new RefreshDelegateInternal(mImageListView.RefreshInternal));
else
mImageListView.RefreshInternal();
m.ReleaseMutex();
}
そして、どこにいてもmImageListViewを破棄しています:
m.WaitOne();
mImageListView.Dispose();
m.ReleaseMutex();
これにより、破棄と呼び出しを同時に行うことができなくなります。
フォーム終了イベントを処理します。オフUIスレッドの作業がまだ行われているかどうかを確認します。まだ行われている場合は、それを停止し始め、終了イベントをキャンセルしてから、フォームコントロールのBeginInvokeを使用して終了を再スケジュールします。
private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
if (service.IsRunning)
{
service.Exit();
e.Cancel = true;
this.BeginInvoke(new Action(() => { this.Close(); }));
}
}
IsakSavoによって提案されたソリューション
try
{
myForm.Invoke(myForm.myDelegate, new Object[] { message });
}
catch (ObjectDisposedException)
{ //catch exception if the owner window is already closed
}
c#4.0で動作しますが、何らかの理由でC#3.0では失敗します(とにかく例外が発生します)
そこで、フォームが閉じているかどうかを示すフラグに基づいて別のソリューションを使用し、その結果、フラグが設定されている場合はinvokeの使用を防止しました
public partial class Form1 : Form
{
bool _closing;
public bool closing { get { return _closing; } }
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_closing = true;
}
...
// part executing in another thread:
if (_owner.closing == false)
{ // the invoke is skipped if the form is closing
myForm.Invoke(myForm.myDelegate, new Object[] { message });
}
これには、try/catchの使用を完全に回避できるという利点があります。
メッセージを生成するスレッドを停止するという提案は受け入れられません。デリゲートはマルチキャストできます。 1人のリスナーがバンドを聴きたくないので、バンドメンバーを撃ちません。フレームワークは、これらのイベントメッセージのメッセージポンプをクリアするための簡単な方法を提供しておらず、フォームは、フォームが閉じていることを通知するプライベートプロパティを公開していないため、次のIsClosingイベントにフラグを設定します。登録を解除するか、イベントのリッスンを停止した後のウィンドウ。this.Invoke()を実行する前に、必ずこのフラグを確認してください。
1つの方法は、ImageListView-Methodを呼び出す代わりに、メソッド自体をもう1つ呼び出すことです。
if (mImageListView != null &&
mImageListView.IsHandleCreated &&
!mImageListView.IsDisposed)
{
if (mImageListView.InvokeRequired)
mImageListView.Invoke(new YourDelegate(thisMethod));
else
mImageListView.RefreshInternal();
}
そうすれば、最終的にRefreshInternal()を呼び出す前に、もう一度チェックします。
これは私のために働きます
if (this.IsHandleCreated){
Task.Delay(500).ContinueWith(_ =>{
this.Invoke(fm2);
});
} else {
this.Refresh();
}
同じエラーが発生します。スレッドでエラーが発生しました。最後に私はこのメソッドを書きます:
public bool IsDisposed(Control ctrl)
{
if (ctrl.IsDisposed)
return true;
try
{
ctrl.Invoke(new Action(() => { }));
return false;
}
catch (ObjectDisposedException)
{
return true;
}
}