3

カスタムSynchronizedCollection<T>クラスをまとめて、WPF アプリケーション用に同期された Observable コレクションを作成できるようにしています。同期は、ほとんどの場合、簡単に適用できる ReaderWriterLockSlim を介して提供されます。私が問題を抱えているのは、コレクションのスレッドセーフな列挙を提供する方法です。次のようなカスタムのIEnumerator<T>ネストされたクラスを作成しました。

    private class SynchronizedEnumerator : IEnumerator<T>
    {
        private SynchronizedCollection<T> _collection;
        private int _currentIndex;

        internal SynchronizedEnumerator(SynchronizedCollection<T> collection)
        {
            _collection = collection;
            _collection._lock.EnterReadLock();
            _currentIndex = -1;
        }

        #region IEnumerator<T> Members

        public T Current { get; private set;}

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            var collection = _collection;
            if (collection != null)
                collection._lock.ExitReadLock();

            _collection = null;
        }

        #endregion

        #region IEnumerator Members

        object System.Collections.IEnumerator.Current
        {
            get { return Current; }
        }

        public bool MoveNext()
        {
            var collection = _collection;
            if (collection == null)
                throw new ObjectDisposedException("SynchronizedEnumerator");

            _currentIndex++;
            if (_currentIndex >= collection.Count)
            {
                Current = default(T);
                return false;
            }

            Current = collection[_currentIndex];
            return true;
        }

        public void Reset()
        {
            if (_collection == null)
                throw new ObjectDisposedException("SynchronizedEnumerator");

            _currentIndex = -1;
            Current = default(T);
        }

        #endregion
    }

ただし、Enumerator が Disposed でない場合、ロックが解除されないことが懸念されます。foreach は Dispose を適切に呼び出す必要があるため、ほとんどのユース ケースではこれは問題になりません。ただし、コンシューマーが明示的な Enumerator インスタンスを取得する場合は問題になる可能性があります。Enumerator を明示的に使用する場合、またはファイナライズ中にロックを安全に解放する方法がある場合、消費者に Dispose を呼び出すことを思い出させる警告実装者でクラスを文書化する唯一のオプションはありますか? ファイナライザーは同じスレッド上でさえ実行されないため、私はそうは考えていませんが、これを改善する他の方法があるかどうか興味がありました.


編集

これについて少し考え、回答を読んだ後 (特にハンスに感謝)、これは間違いなく悪い考えだと判断しました。最大の問題は、実際には Dispose を忘れていることではなく、のんびりとした消費者が列挙中にデッドロックを作成していることです。コピーを取得し、コピーの列挙子を返すのに十分な時間だけ読み取りロックします。

4

4 に答える 4

3

そうです、それは問題です。ファイナライザーは役に立ちません。実行が遅すぎて役に立ちません。とにかく、コードはその前にひどくデッドロックしているはずです。残念ながら、MoveNext/Current メンバーを呼び出す foreach と、それらを明示的に使用するクライアント コードとの違いを見分ける方法はありません。

修正はありません。これを行わないでください。Microsoft もそれをしませんでした。.NET 1.x に戻る十分な理由がありました。作成できる真のスレッド セーフ イテレータは、GetEnumerator() メソッドでコレクション オブジェクトのコピーを作成するイテレータだけです。ただし、イテレータがコレクションと同期しなくなるのも楽しいことではありません。

于 2010-04-11T15:34:41.733 に答える
2

これは私には非常にエラーが発生しやすいようです。これは、ロックが暗示的/サイレントに取り出される状況を助長し、コードの読者には明らかではなく、インターフェイスに関する重要な事実が誤解される可能性が高くなります。

通常、一般的なパターンを複製することをお勧めします - 処理が終わったら破棄される列挙可能なコレクションを表します - しかし、残念ながらIEnumerable<T>、ロックを取り出すという追加の要素が大きな違いを生みます。

理想的なアプローチは、スレッド間で共有されるコレクションの列挙をまったく提供しないことです。システム全体を設計して、それが不要になるようにしてください。明らかに、これは時々クレイジーな夢物語になるでしょう。

したがって、次善の策はIEnumerable<T>、ロックが存在する間、 が一時的に利用できるコンテキストを定義することです。

public class SomeCollection<T>
{
    // ...

    public void EnumerateInLock(Action<IEnumerable<T>> action) ...

    // ...
}

つまり、このコレクションのユーザーがそれを列挙したい場合、次のようにします。

someCollection.EnumerateInLock(e =>
    {
        foreach (var item in e)
        {
            // blah
        }
    });

これにより、ロックの有効期間がスコープによって明示的に指定され (ラムダ本体で表され、lockステートメントのように機能します)、破棄するのを忘れて誤って延長されることがなくなります。このインターフェースを悪用することは不可能です。

メソッドの実装は次のEnumerateInLockようになります。

public void EnumerateInLock(Action<IEnumerable<T>> action)
{
    var e = new EnumeratorImpl(this);

    try
    {
        _lock.EnterReadLock();
        action(e);
    }
    finally
    {
        e.Dispose();
        _lock.ExitReadLock();
    }
}

EnumeratorImpl(独自の特定のロック コードを必要としない) は、ロックが解除される前に常に破棄されることに注意してください。破棄後、任意のメソッド呼び出し (無視されるObjectDisposedException以外) に応答してスローします。Dispose

これは、インターフェースを悪用しようとする試みがあっても、次のことを意味します。

IEnumerable<C> keepForLater = null;
someCollection.EnumerateInLock(e => keepForLater = e);

foreach (var item in keepForLater)
{
    // aha!
}

これは、タイミングに基づいて時々不思議に失敗するのではなく、常にスローします。

このようにデリゲートを受け入れるメソッドを使用することは、Lisp やその他の動的言語で一般的に使用されるリソースの有効期間を管理するための一般的な手法であり、実装よりも柔軟性が低くなりIDisposableますが、柔軟性の低下は多くの場合祝福です: クライアントに対する懸念が取り除かれます。"捨て忘れ」。

アップデート

あなたのコメントから、コレクションへの参照を既存のUIフレームワークに渡すことができる必要があることがわかりました。したがって、コレクションへの通常のインターフェースを使用できる、つまり、コレクションから直接取得しIEnumerable<T>て信頼できることが期待されますすばやくきれいにします。その場合、心配する必要はありません。UI フレームワークを信頼して、UI を更新し、コレクションを迅速に破棄します。

他の唯一の現実的なオプションは、列挙子が要求されたときにコレクションのコピーを作成することです。そうすれば、コピーが作成されているときにのみロックを保持する必要があります。準備が整うとすぐに、ロックが解除されます。コレクションが通常小さい場合、これはより効率的である可能性があります。そのため、コピーのオーバーヘッドは、短いロックによるパフォーマンスの節約よりも少なくなります。

単純なルールを使用することを (約 1 ナノ秒) 提案したくなるかもしれません。コレクションがしきい値よりも小さい場合はコピーを作成し、それ以外の場合は元の方法で作成します。実装を動的に選択します。そうすれば、最適なパフォーマンスを得ることができます - コピーがロックを保持するよりも安価になるように (実験によって) しきい値を設定します。ただし、スレッド化されたコードでのこのような「賢い」アイデアについては、常に 2 回 (または 10 億回) 考えていました。捨てるのを忘れてしまっても大量のコレクションじゃない限り問題にならない… 隠れたバグのレシピ。そこに行かないでください!

「コピーを公開する」アプローチのもう 1 つの潜在的な欠点は、アイテムがコレクション内にある場合は世界に公開されているが、コレクションから削除されるとすぐに安全に隠されているという仮定にクライアントが間違いなく陥ることです。これは今では間違っているでしょう!UI スレッドが列挙子を取得すると、バックグラウンド スレッドがその最後の項目を削除し、それが削除されたために他の誰も見ることができないという誤った信念に基づいて変更を開始します。

そのため、コピーのアプローチでは、コレクションのすべてのアイテムが独自の同期を効果的に持つ必要があります。ほとんどのコーダーは、代わりにコレクションの同期を使用することでこれをショートカットできると想定します。

于 2010-04-11T15:22:14.600 に答える
1

使用する必要のある実装では、使用する管理されていないリソースを常に破棄するIDisposable保護されたメソッドを作成します。Dispose(bool managed)ファイナライザーから保護されたDispose(false)メソッドを呼び出すことにより、必要に応じてロックを破棄します。ロックは管理されています。Dispose(true)呼び出されたときにのみ破棄しますか。trueつまり、管理対象オブジェクトを破棄する必要があります。それ以外の場合、publicDispose()が明示的に呼び出されると、protectedが呼び出されDispose(true)GC.SuppressFinalize(this)ファイナライザーが実行されないようにします(これ以上破棄するものがないため)。

ユーザーが列挙子をいつ使用したかわからないため、ユーザーがオブジェクトを破棄する必要があることを文書化する以外に選択肢はありません。ユーザーが構築を使用することを提案することをお勧めしますusing(){ ... }。これにより、完了時にオブジェクトが自動的に破棄されます。

于 2010-04-11T14:57:10.280 に答える
1

私は最近これをしなければなりませんでした。私が行った方法は、実際のリスト/配列カウント(および実装)の両方を含む内部オブジェクト(参照)が存在するように抽象化することでした。次に、次のようにすることで、ロックフリーでスレッドセーフな列挙を実行できます。GetEnumerator()

public IEnumerator<T> GetEnumerator() { return inner.GetEnumerator();}

Addなどを同期する必要がありますが、参照が変更されますinner(参照の更新はアトミックであるため、同期する必要はありませんGetEnumerator())。これは、列挙子が作成されたときに存在していたのと同じ数のアイテムを列挙子が返すことを意味します。

もちろん、それは私のシナリオが単純であったことを助けます、そして私のリストはAddただ...あなたが突然変異/削除をサポートする必要があるならば、それははるかにトリッキーです。

于 2010-04-11T15:14:19.907 に答える