同期された IEnumerator<T>
-
26-09-2019 - |
質問
カスタムをまとめています 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
}
ただし、私の懸念は、列挙子が破棄されないとロックが決して解放されないことです。ほとんどの使用例では、foreach は Dispose を適切に呼び出す必要があるため、これは問題になりません。ただし、コンシューマーが明示的な Enumerator インスタンスを取得する場合、問題が発生する可能性があります。Enumerator を明示的に使用する場合に Dispose を呼び出すようにコンシューマーに通知する警告実装を使用してクラスを文書化する唯一のオプションはありますか、それともファイナライズ中に安全にロックを解放する方法はありますか?ファイナライザーは同じスレッド上でさえ実行されないため、そうではないと考えていますが、これを改善する他の方法があるかどうか興味がありました。
編集
これについて少し考えて、回答を読んだ後(特にハンスに感謝します)、これは間違いなく悪いアイデアであると判断しました。実際、最大の問題は Dispose を忘れることではなく、むしろ、消費者が列挙中にのんびりとデッドロックを引き起こしてしまうことです。現在は、コピーを取得してコピーの列挙子を返すのに十分な期間のみ読み取りロックを行っています。
解決
あなたは問題だと、正しいです。ファイナライザは無用である、それはどんな有用であることがあまりにも遅く実行されます。コードは、とにかく前に多額のデッドロックしている必要があります。残念ながら、あなたが明示的にそれらを使用して、MoveNextメソッド/現在のメンバーやクライアントのコードを呼び出すforeachの違いを教えするための方法はありません。
はありません修正は、これをしません。 Microsoftはどちらかそれをしなかった、彼らは、.NET 1.xで背中に理由の多くを持っていましたあなたが作ることができる唯一の本当のスレッドセーフなイテレータは、GetEnumeratorメソッド()メソッドでは、コレクションオブジェクトのコピーを作成するものです。コレクションと同期して取得するイテレータはどちらかしかし何の喜びはありません。
他のヒント
これは私にとってあまりにも間違いが起こりやすいように思えます。これは、コードの読者には明確ではない方法でロックが暗黙的/サイレントに解除される状況を助長し、インターフェイスに関する重要な事実が誤解される可能性を高めます。
通常は、共通のパターンを複製することをお勧めします。つまり、列挙可能なコレクションを次のように表します。 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 を更新し、コレクションを迅速に破棄します。
他の唯一の現実的なオプションは、列挙子が要求されたときに単純にコレクションのコピーを作成することです。こうすることで、コピーの作成時にのみロックを保持する必要があります。準備が完了するとすぐにロックが解除されます。コレクションが通常小さいため、コピーのオーバーヘッドが、ロックが短いことによるパフォーマンスの節約よりも小さい場合、これはより効率的である可能性があります。
(約ナノ秒間) 次のような単純なルールを使用することを提案したくなります。コレクションがあるしきい値より小さい場合はコピーを作成し、それ以外の場合は元の方法で実行します。実装を動的に選択します。そうすることで、最適なパフォーマンスが得られます。ロックを保持するよりもコピーのコストが低くなるように、(実験により) しきい値を設定します。ただし、スレッド コードにおけるそのような「賢い」アイデアについては、常に 2 回 (または 10 億回) 考えます。列挙子の悪用がどこかにあった場合はどうなるでしょうか。捨て忘れても問題ない 大規模なコレクションでない限り...隠れたバグのレシピ。そこには行かないでください!
「コピーを公開する」アプローチのもう 1 つの潜在的な欠点は、アイテムがコレクション内にある場合は世界に公開されているが、コレクションから削除されるとすぐに安全に隠蔽されるという思い込みにクライアントが間違いなく陥ることです。これはもう間違っているでしょう!UI スレッドが列挙子を取得すると、バックグラウンド スレッドがその列挙子から最後の項目を削除し、削除されたため他の誰も見ることができないという誤った信念のもと、その項目の変更を開始します。
したがって、コピーのアプローチでは、コレクション上のすべての項目が独自の同期を効率的に行う必要がありますが、ほとんどのプログラマーは、代わりにコレクションの同期を使用することでこれをショートカットできると想定します。
最近これをやらなければならなかったのです。私がそれを行った方法は、それを抽象化して、以下を含む内部オブジェクト(参照)が存在するようにすることでした。 両方 実際のリスト/配列 そして カウント (および GetEnumerator()
実装;次に、次のようにすることで、ロックフリーでスレッドセーフな列挙を実行できます。
public IEnumerator<T> GetEnumerator() { return inner.GetEnumerator();}
の Add
などを同期する必要がありますが、 変化 の inner
参照 (参照の更新はアトミックであるため、 しないでください 同期する必要がある GetEnumerator()
)。これは、どの列挙子もそこに存在したのと同じ数の項目を返すことを意味します。 列挙子が作成されたとき.
もちろん、私のシナリオがシンプルで、リストが次のとおりだったことが役に立ちました。 Add
のみ...変更/削除をサポートする必要がある場合は、さらに複雑になります。
は必見使用IDisposable
の実装では、あなたはいつもあなたが使用しているアンマネージリソースを配置し、保護Dispose(bool managed)
メソッドを作成します。 ストライキ>必要に応じて<ストライキ>、あなたのファイナライザからあなたの保護Dispose(false)
メソッドを呼び出すことによって、あなたはロックを処分します。ロックが管理されているDispose(true)
がどこtrue
手段、呼び出されたとき、あなたは管理対象オブジェクトを配置する必要があるということだけを処分しましょうか。公共Dispose()
が明示的に呼び出されたときにそれ以外の場合、それは(もう処分するものがないので)を実行しているからファイナライザを防ぐためにDispose(true)
も保護GC.SuppressFinalize(this)
を呼び出します。
は、ユーザーがオブジェクトを配置しなければならないことを文書化するよりも、他のオプションが用意されていません。あなたは、ユーザーが実行時に自動的にオブジェクトを配置using(){ ... }
構造を、使用することを提案したい場合があります。