代码之家  ›  专栏  ›  技术社区  ›  Dan Bryant

同步IEnumerator<t>

  •  3
  • Dan Bryant  · 技术社区  · 14 年前

    我正在整理一个习惯 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,还是在终结过程中有安全释放锁的方法?我不这么认为,因为定稿器甚至不会运行在同一个线程上,但是我很好奇是否还有其他方法可以改进这一点。


    编辑

    在考虑了这一点并阅读了回复(特别是感谢汉斯),我决定这绝对是个坏主意。最大的问题实际上不是忘记处理,而是一个悠闲的消费者在枚举时创建死锁。我现在只读取足够长的锁来获取副本并返回副本的枚举器。

    4 回复  |  直到 14 年前
        1
  •  3
  •   Hans Passant    14 年前

    你说得对,这是个问题。定稿器是无用的,它将运行得太晚,无法使用。无论如何,在这之前,代码应该已经严重死锁了。不幸的是,您无法区分a foreach调用movenext/current成员或显式使用它们的客户机代码之间的区别。

    不解决,不要这样做。微软也没有这样做,他们有足够的理由回到.NET 1.x中。唯一真正的线程安全迭代器是在getEnumerator()方法中创建集合对象的副本。不过,与集合不同步的迭代器也不是什么乐趣。

        2
  •  2
  •   Daniel Earwicker    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 (不需要特定的锁代码)总是在退出锁之前释放。处理后,它抛出 ObjectDisposedException 响应任何方法调用(除了 Dispose ,这将被忽略。)

    这意味着即使有人试图滥用接口:

    IEnumerable<C> keepForLater = null;
    someCollection.EnumerateInLock(e => keepForLater = e);
    
    foreach (var item in keepForLater)
    {
        // aha!
    }
    

    本遗嘱 总是 抛掷,而不是根据时间而神秘地失败。

    使用接受这样的委托的方法是管理Lisp和其他动态语言中常用的资源生命周期的一种通用技术,尽管它不如实现 IDisposable 这种灵活性的降低常常是一种福音:它消除了客户“忘记处理”的顾虑。

    更新

    从您的评论中,我看到您需要能够将对集合的引用传递给现有的UI框架,从而期望能够使用集合的常规接口,即直接获取 IEnumerable<t> 相信能迅速清理干净。在这种情况下,为什么要担心?信任UI框架以更新UI并快速处置集合。

    您唯一实际的选择是在请求枚举器时复制集合。这样,只有在复制时才需要保持锁定。一旦准备好,锁就被释放了。如果集合通常很小,这可能更有效,因此由于锁更短,复制的开销小于性能节省。

    建议您使用一个简单的规则是很有吸引力的(大约一纳秒):如果集合小于某个阈值,则进行复制,否则以原始方式执行;动态选择实现。这样您就获得了最佳的性能—通过实验设置阈值,这样拷贝就比持有锁便宜。然而,对于线程代码中的这种“聪明”的想法,我总是会三思而后行(或十亿次),因为如果在某个地方存在对枚举器的滥用呢?如果你忘了处理它,就不会有问题了 除非它是一个大收藏 …隐藏虫子的配方。不要去那儿!

    “公开一份副本”方法的另一个潜在缺点是,客户无疑会陷入这样的假设:如果某个项目在集合中,它将暴露于世界,但一旦从集合中删除,它将被安全地隐藏。现在就错了!UI线程将获得一个枚举器,然后我的后台线程将从中删除最后一个项,然后开始对其进行变异,错误地认为,因为它被删除了,所以没有其他人能够看到它。

    因此,复制方法要求集合中的每个项都有效地拥有自己的同步,在这里,大多数编码人员将假定他们可以通过使用集合的同步来实现这一点。

        3
  •  1
  •   Marc Gravell    14 年前

    我最近不得不这么做。我这样做的方法是抽象它,这样就有一个内部对象(引用)包含 二者都 实际列表/数组 计数(和A GetEnumerator() 实现;然后我可以执行无锁、线程安全的枚举,方法是:

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

    这个 Add ETC需要同步,但它们 改变 这个 inner 引用(因为引用更新是原子的,所以 不要 需要同步 GET枚举器() )这意味着任何枚举器都将返回与以前一样多的项 创建枚举器时 .

    当然,我的方案很简单,我的清单 添加 只有…如果您需要支持mutate/remove,那么它就更加棘手了。

        4
  •  1
  •   Daniel A.A. Pelsmaeker    14 年前

    在必须使用的 IDisposable 实现时,您将创建一个受保护的 Dispose(bool managed) 方法,它总是处理您使用的非托管资源。通过呼叫受保护的 Dispose(false) 方法来自终结器, 你要按要求把锁处理掉 . 锁是管理好的,只有在 Dispose(true) 在哪里打电话 true 意味着需要释放托管对象。否则,当公众 Dispose() 显式调用,它调用受保护的 处置(真) 而且 GC.SuppressFinalize(this) 以防止终结器运行(因为不再有任何要处理的内容)。

    因为您永远不知道用户何时使用枚举器,所以除了记录用户必须处理对象之外,您没有其他选项。您可能希望建议用户使用 using(){ ... } 构造,完成后自动处理对象。

    推荐文章