我正在设置一个定制的SynchronizedCollection<T>类,这样我就可以为我的WPF应用程序提供一个同步的可观察集合。同步是通过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。但是,如果使用者检索显式枚举数实例,则可能会出现问题。我唯一的选择是用警告实现者记录类,提醒使用者在显式使用枚举数时调用Dispose,还是有一种方法可以在终结过程中安全地释放锁?我不这么认为,因为终结器甚至不运行在同一个线程上,但是我很好奇是否还有其他方法来改进它。
编辑
在考虑了这一点,并阅读了回复(特别是感谢汉斯)后,我认为这绝对是个坏主意。最大的问题实际上并不是忘记释放,而是一个悠闲的消费者在枚举时创建死锁。我现在只读锁足够长的时间来获得副本并返回副本的枚举数。
发布于 2010-04-11 15:34:41
你说得对,这是个问题。终结器是无用的,它将运行太晚,没有任何用处。无论如何,在此之前,代码应该已经陷入了严重的僵局。不幸的是,您无法区分调用MoveNext/当前成员的前端代码与显式使用它们的客户端代码之间的区别。
没办法,别这么做。微软也没有这么做,他们有充分的理由回到.NET 1.x。您可以制作的唯一真正的线程安全迭代器是在GetEnumerator()方法中创建集合对象的副本。但是,迭代器与集合不同步也不是joy。
发布于 2010-04-11 15:22:14
这对我来说似乎太容易出错了。它鼓励以代码读者不清楚的方式隐式地/静默地取出锁的情况,并使有关接口的关键事实可能被误解。
通常,复制公共模式是个好主意--用IEnumerable<T>表示一个可枚举的集合,当您完成它时就会处理它--但不幸的是,取出锁的附加成分带来了很大的不同。
我建议理想的方法是完全不对线程间共享的集合提供枚举。试着设计整个系统,这样就不需要了。很明显,有时候这会是个疯狂的白日梦。
因此,下一个最好的方法是定义一个上下文,其中IEnumerable<T>是临时可用的,而锁是存在的:
public class SomeCollection<T>
{
// ...
public void EnumerateInLock(Action<IEnumerable<T>> action) ...
// ...
}也就是说,当这个集合的用户想要枚举它时,他们会这样做:
someCollection.EnumerateInLock(e =>
{
foreach (var item in e)
{
// blah
}
});这使得由作用域(由lambda主体表示,工作非常像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 (它自己不需要特定的锁定代码)总是在锁退出之前被释放。处理后,它将对任何方法调用( Dispose除外)抛出ObjectDisposedException,该方法调用将被忽略。
这意味着,即使有人试图滥用接口:
IEnumerable<C> keepForLater = null;
someCollection.EnumerateInLock(e => keepForLater = e);
foreach (var item in keepForLater)
{
// aha!
}这将永远抛出,而不是神秘地失败,有时根据时间。
使用像这样接受委托的方法是管理Lisp和其他动态语言中常用的资源生存期的通用技术,虽然它比实现IDisposable不那么灵活,但降低灵活性通常是一件好事:它消除了对客户端“忘记处理”的担忧。
更新
从您的评论中,我看到您需要能够将对集合的引用传递给现有的UI框架,因此期望能够使用集合的正常接口,即直接从集合中获取IEnumerable<T>并信任它快速清理它。既然如此,何必担心呢?信任UI框架更新UI并快速释放集合。
您唯一的其他实际选项是,在请求枚举数时,只需制作集合的副本。这样,锁只需要在复制时保持。一旦它准备好,锁就会被释放。如果集合通常较小,这可能会更有效,因此由于锁较短,副本的开销小于性能节省。
建议您使用一条简单的规则是很有诱惑力的(大约1纳秒):如果集合小于某个阈值,则复制,否则按原来的方式执行;动态选择实现。这样,您就可以获得最佳的性能--通过实验设置阈值,以便副本比保持锁更便宜。但是,对于线程代码中的这种“聪明”想法,我总是三思(或十亿次),因为如果枚举器在某个地方被滥用了怎么办?如果你忘了处理它,你就不会发现问题,除非它是一个庞大的收藏.隐藏虫子的秘方。别去那儿!
“公开副本”方法的另一个潜在缺点是,客户无疑会假定,如果一个项目在集合中,它就会暴露在世界上,但一旦从集合中移除,它就会安全地隐藏起来。现在这就错了!UI线程将获得一个枚举数,然后我的后台线程将从其中移除最后一项,然后开始对其进行变异,因为错误地认为,由于它被移除,其他人看不到它。
因此,复制方法要求集合中的每一项都有效地具有自己的同步,其中大多数编码者将假设他们可以通过使用集合的同步来快捷这一点。
发布于 2010-04-11 15:14:19
我最近不得不这么做。我这样做的方法是抽象它,以便有一个包含的内部对象(引用)--、实际的列表/数组和计数(以及一个GetEnumerator()实现;然后我可以通过以下方式进行无锁、线程安全的枚举:
public IEnumerator<T> GetEnumerator() { return inner.GetEnumerator();}Add等需要同步,但是它们改变了-- inner引用(因为引用更新是原子的,所以不需要同步GetEnumerator())。这意味着任何枚举数都将返回与创建枚举器时相同的项。
当然,这有助于我的方案是简单的,我的列表是Add .如果你需要支持变异/删除,那么它就更棘手了。
https://stackoverflow.com/questions/2617400
复制相似问题