c# 自作 - IDisposableオブジェクトにスレッドセーフティを追加する正しい方法は何ですか?




3 Answers

あなたができる最も簡単なことは、privateに配置された変数をvolatileとしてマークし、メソッドの始めに検査します。 オブジェクトがすでにObjectDisposedExceptionされている場合は、 ObjectDisposedExceptionをスローできます。

これには2つの注意点があります。

  1. メソッドがイベントハンドラである場合は、 ObjectDisposedExceptionスローしないでください。 代わりに、可能であればメソッドから正常に終了する必要があります。 その理由は、あなたがそれらから退会した後にイベントを提起できる競争状態が存在するからです。 (詳細については 、Eric Lippertの記事を参照してください)。

  2. これにより、クラスメソッドの実行中にクラスが破棄されることはなくなります。 したがって、クラスに処分後にアクセスできないインスタンスメンバーがある場合、これらのリソースへのアクセスが制御されるようにロック動作を設定する必要があります。

IDisposableに関するマイクロソフトのガイダンスによれば、すべての方法で処理されているかどうかを確認する必要があると言われています 実際には、クラスが破棄された後にメソッドを実行できるようにすると、例外がスローされたり、意図しない副作用が発生することがあります。 答えが「はい」の場合は、それが起こらないことを確認するために何らかの作業を行う必要があります。

すべてのIDisposableクラスをスレッドセーフにする必要があるかどうかという点では:いいえ、使い捨てクラスの使用例の大部分は、単一のスレッドによってアクセスされただけです。

つまり、ディスポーザブルクラスをスレッドセーフにする必要がある理由を調べると、複雑さが増します。 あなたの使い捨てクラスのスレッドの安全性の問題を心配する必要がないようにする別の実装があるかもしれません。

クラス 解放

いくつかのパブリックメソッドを持つIDisposableインターフェイスの実装を想像してみてください。

そのタイプのインスタンスが複数のスレッド間で共有されていて、スレッドの1つがそれを破棄できる場合、他のスレッドが破棄後にそのインスタンスで動作しないようにする最良の方法は何ですか? ほとんどの場合、オブジェクトが破棄された後、そのメソッドはそれを認識し、 ObjectDisposedExceptionまたはおそらくInvalidOperationExceptionをスローするか、少なくとも何か問題が起きたことを呼び出しコードに通知する必要があります。 すべての方法 - 特に小切手が処分されている場合 - の周りに同期が必要ですか? 他のパブリックメソッドを持つすべての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?
    }
}



私は、Disposが既に呼び出されているかどうかをテストするために、スレッドセーフなInterlockedクラスを使用することができるため、破棄されたステータスを格納するフィールドとしてブール値ではなく整数を使用する傾向があります。

このようなもの:

private volatile int _disposeCount;

public void Dispose()
{
    if (Interlocked.Increment(ref _disposeCount) == 1)
    {
        // disposal code here
    }
}

これにより、処理コードは、メソッドの呼び出し回数にかかわらず一度だけ呼び出され、完全にスレッドセーフです。

次に、各メソッドは非常に単純にバリアメソッドとしてこのメ​​ソッドを呼び出すことができます:

private void ThrowIfDisposed()
{
   if (_disposeCount > 0) throw new ObjectDisposedException(GetType().Name);
}

すべてのメソッドを同期することに関して、単純なバリアチェックではできないと言っていますが、インスタンス内ですでにコードを実行している可能性のある他のスレッドを停止したいとしますか? これはもっと複雑な問題です。 あなたのコードが何をしているのか分かりませんが、本当に必要な場合は考慮してください。

あなたが破棄された小切手自体に関してちょうど意味したら - 上の私の例はうまくいます。

編集:コメントに答える "これと揮発性ブールフラグの違いは何ですか?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を使用すると、割り当てと後続の読み取りの両方が単一のアトミック操作になります。




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()メソッドを呼び出します。

エラー状態ではなく、処理上のスレッド競合が発生している可能性が高いと思われる場合は、公開IsDisposed()メソッドも追加しIsDisposed() Control.IsDisposedと同様)。

Update: isDisposed volatileにするという価値に関するコメントに基づいて、 CheckDisposed()メソッドの使い方を考えると、「フェンス」の問題はCheckDisposed()です。 これは、オブジェクトがすでに破棄された後にコードがパブリックメソッドを呼び出す場合を素早くキャッ​​チするためのトラブルシューティングツールです。 パブリックメソッドの開始時にCheckDisposed()を呼び出すことは、そのメソッド内にオブジェクトが配置されないことを保証するものではありません。 私が考慮しなかったエラー状態とは対照的に、クラスの設計に固有のリスクであると考えると、前述のIsDisposedメソッドと適切なロックを使用します。




Related

c# .net thread-safety idisposable