いくつかのパブリックメソッドを持つIDisposable
インターフェースの実装を想像してみてください。
そのタイプのインスタンスが複数のスレッド間で共有されており、スレッドの1つがそれを破棄する可能性がある場合、他のスレッドが破棄後にインスタンスを操作しようとしないようにするための最良の方法は何ですか?ほとんどの場合、オブジェクトが破棄された後、そのメソッドはオブジェクトを認識し、ObjectDisposedException
またはおそらくInvalidOperationException
をスローするか、少なくとも呼び出し元のコードに何か問題があったことを通知する必要があります。 everyメソッドの同期が必要ですか?特にそれが破棄されているかどうかのチェックの前後ですか?他のパブリックメソッドを使用したすべてのIDisposable
実装は、スレッドセーフである必要がありますか?
次に例を示します。
public class DummyDisposable : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
_disposed = true;
// actual dispose logic
}
public void DoSomething()
{
// maybe synchronize around the if block?
if (_disposed)
{
throw new ObjectDisposedException("The current instance has been disposed!");
}
// DoSomething logic
}
public void DoSomethingElse()
{
// Same sync logic as in DoSomething() again?
}
}
最も簡単な方法は、破棄されたプライベート変数をvolatile
としてマークし、メソッドの最初でそれを検査することです。オブジェクトがすでに破棄されている場合は、ObjectDisposedException
をスローできます。
これには2つの注意点があります。
メソッドがイベントハンドラーの場合は、ObjectDisposedException
をスローしないでください。代わりに、可能であれば、メソッドを正常に終了する必要があります。その理由は、イベントの購読を解除した後にイベントが発生する可能性がある競合状態が存在するためです。 (詳細については、Eric Lippertによる この記事 を参照してください。)
これは、クラスメソッドの1つを実行している最中に、クラスが破棄されるのを防ぐことはできません。したがって、破棄後にアクセスできないインスタンスメンバーがクラスにある場合は、これらのリソースへのアクセスが確実に制御されるように、いくつかのロック動作を設定する必要があります。
IDisposableに関するMicrosoftのガイダンスでは、すべてのメソッドで破棄されているかどうかを確認する必要があるとされていますが、個人的にはこれが必要であるとは思っていません。問題は、クラスが破棄された後にメソッドの実行を許可すると、例外がスローされたり、意図しない副作用が発生したりすることです。答えが「はい」の場合、それが起こらないようにするためにいくつかの作業を行う必要があります。
すべてのIDisposableクラスがスレッドセーフである必要があるかどうかに関して:いいえ。使い捨てクラスのほとんどのユースケースでは、単一のスレッドからのみアクセスされます。
そうは言っても、使い捨てクラスがスレッドセーフである必要がある理由を調査することをお勧めします。使い捨てクラスのスレッドセーフの問題を心配する必要がない別の実装があるかもしれません。
DisposeのほとんどのBCL実装は、スレッドセーフではありません。アイデアは、Disposeの呼び出し元が、Disposeされる前に他の誰もインスタンスを使用していないことを確認することです。言い換えれば、それは同期の責任を押し上げます。これは理にかなっています。そうでなければ、他のすべてのコンシューマーは、オブジェクトの使用中にオブジェクトが破棄された境界ケースを処理する必要があります。
とはいえ、スレッドセーフなDisposableクラスが必要な場合は、上部に_disposedをチェックして、すべてのパブリックメソッド(Disposeを含む)の周りにロックを作成できます。メソッド全体のロックを保持したくない長時間実行メソッドがある場合、これはより複雑になる可能性があります。
破棄ステータスを格納するフィールドとしてブール値ではなく整数を使用する傾向があります。これは、スレッドセーフなInterlockedクラスを使用して、Disposeがすでに呼び出されているかどうかをテストできるためです。
このようなもの:
private int _disposeCount;
public void Dispose()
{
if (Interlocked.Increment(ref _disposeCount) == 1)
{
// disposal code here
}
}
これにより、メソッドが何度呼び出されても、廃棄コードが1回だけ呼び出され、完全にスレッドセーフになります。
次に、各メソッドは、このメソッドをバリアチェックとして呼び出すことを非常に簡単に使用できます。
private void ThrowIfDisposed()
{
if (_disposeCount > 0) throw new ObjectDisposedException(GetType().Name);
}
すべてのメソッドの同期に関して-単純なバリアチェックでは機能しないと言っていますか-インスタンスですでにコードを実行している可能性のある他のスレッドを停止したい。これはもっと複雑な問題です。あなたのコードが何をしているのかわかりませんが、本当にそれが必要かどうかを検討してください-単純なバリアチェックではうまくいきませんか?
廃棄された小切手自体に関して意図しているのであれば、上記の私の例は問題ありません。
EDIT:「これと揮発性のboolフラグの違いは何ですか?somethingCountという名前のフィールドがあり、0と1の値のみを保持できるようにするのは少し混乱します」というコメントに答える
Volatileは、読み取りまたは書き込み操作操作がアトミックで安全であることを保証することに関連しています。値スレッドの割り当ておよびのプロセスを安全にするわけではありません。したがって、たとえば、以下は揮発性にもかかわらずスレッドセーフではありません。
private volatile bool _disposed;
public void Dispose()
{
if (!_disposed)
{
_disposed = true
// disposal code here
}
}
ここでの問題は、2つのスレッドが接近している場合、最初のスレッドが_disposedをチェックし、falseを読み取り、コードブロックに入り、_disposedをtrueに設定する前に切り替えられる可能性があることです。次に、2番目は_disposedをチェックし、falseを確認して、コードブロックに入ります。
Interlockedを使用すると、割り当てとその後の読み取りの両方が単一のアトミック操作であることが保証されます。
整数型オブジェクトの「disposed」または「state」変数で整数とInterlocked.Exchange
またはInterlocked.CompareExchange
を使用することを好みます。 Interlocked.Exchange
またはInterlocked.CompareExchange
がそのようなタイプを処理できる場合は、enum
を使用しますが、残念ながらできません。
IDisposableとファイナライザーのほとんどの議論で言及されていない1つのポイントは、IDisposable.Dispose()の進行中はオブジェクトのファイナライザーを実行すべきではないが、クラスがそのタイプのオブジェクトがデッドと宣言されてからデッドと宣言されるのを防ぐ方法がないということです。復活した。確かに、外部コードがそれを可能にする場合、オブジェクトが「正常に機能する」という要件は明らかにあり得ませんが、Disposeメソッドとfinalizeメソッドは、それらが破損しないように十分に保護されている必要があります その他 オブジェクトの状態。通常、オブジェクトの状態変数に対してロックまたはInterlocked
操作を使用する必要があります。
FWIW、あなたのサンプルコードは、私の同僚と私が通常この問題に対処する方法と一致しています。通常、クラスでプライベートCheckDisposed
メソッドを定義します。
_private volatile bool isDisposed = false; // Set to true by Dispose
private void CheckDisposed()
{
if (this.isDisposed)
{
throw new ObjectDisposedException("This instance has already been disposed.");
}
}
_
次に、すべてのパブリックメソッドの先頭でCheckDisposed()
メソッドを呼び出します。
エラー状態ではなく、破棄をめぐるスレッドの競合が発生する可能性が高いと考えられる場合は、public IsDisposed()
メソッドも追加します( Control.IsDisposed と同様)。
更新:isDisposed
を揮発性にすることの価値に関するコメントに基づいて、CheckDisposed()
メソッドの使用方法を考えると、「フェンス」の問題はかなり些細なことに注意してください。これは本質的に、オブジェクトが既に破棄された後、コードがオブジェクトのパブリックメソッドを呼び出すケースをすばやくキャッチするためのトラブルシューティングツールです。パブリックメソッドの開始時にCheckDisposed()
を呼び出しても、オブジェクトがそのメソッド内に配置されないことを保証するものではありません。私が説明できなかったエラー状態とは対照的に、それがクラスの設計に固有のリスクであると考える場合は、適切なロックとともに前述のIsDisposed
メソッドを使用します。
破棄するリソースへのすべてのアクセスをロックする必要があります。また、普段使っているDisposeパターンも追加しました。
public class MyThreadSafeClass : IDisposable
{
private readonly object lockObj = new object();
private MyRessource myRessource = new MyRessource();
public void DoSomething()
{
Data data;
lock (lockObj)
{
if (myResource == null) throw new ObjectDisposedException("");
data = myResource.GetData();
}
// Do something with data
}
public void DoSomethingElse(Data data)
{
// Do something with data
lock (lockObj)
{
if (myRessource == null) throw new ObjectDisposedException("");
myRessource.SetData(data);
}
}
~MyThreadSafeClass()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (disposing)
{
lock (lockObj)
{
if (myRessource != null)
{
myRessource.Dispose();
myRessource = null;
}
}
//managed ressources
}
// unmanaged ressources
}
}