代码之家  ›  专栏  ›  技术社区  ›  Codebrain

多线程同步列表<t>

  •  5
  • Codebrain  · 技术社区  · 15 年前

    我有一个要求,我需要存储一个简单的项目列表缓存。为此,我使用了list<t>,但现在我们已经更改了设计以适应多线程。

    系统的体系结构是由事件驱动的,因此读写操作很可能会发生冲突。因为绝大多数访问都是只读的,所以我认为读写器lockslim可能是一个很好的候选者。缓存只需要在读取时及时准确。

    我已经写了下面的代码,它看起来工作正常,但是有一些潜在的痛点吗?

    更新:虽然下面的代码确实解决了一些同步问题,但并不是100%完美。此后,我决定实现一个更简单的类,该类不会公开所有的IList<t>操作,因此使重用变得“更安全”。

    public class SynchronisedList<T> : IList<T>
    {
        private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
        private IList<T> innerCache = new List<T>();
    
        private U ReadReturn<U>(Func<U> function)
        {
            cacheLock.EnterReadLock();
            try { return function(); }
            finally { cacheLock.ExitReadLock(); }
        }
    
        private void Read(Action action)
        {
            cacheLock.EnterReadLock();
            try { action(); }
            finally { cacheLock.ExitReadLock(); }
        }
    
        private U WriteReturn<U>(Func<U> function)
        {
            cacheLock.EnterWriteLock();
            try { return function(); }
            finally { cacheLock.ExitWriteLock(); }
        }
    
        private void Write(Action action)
        {
            cacheLock.EnterWriteLock();
            try { action(); }
            finally { cacheLock.ExitWriteLock(); }
        }
    
        public T this[int index]
        {
            get { return ReadReturn(() => innerCache[index]); }
            set { Write(() => innerCache[index] = value); }
        }
    
        public int IndexOf(T item) { return ReadReturn(() => innerCache.IndexOf(item)); }
        public void Insert(int index, T item) { Write(() => innerCache.Insert(index, item)); }
        public void RemoveAt(int index) { Write(() => innerCache.RemoveAt(index)); }
        public void Add(T item) { Write(() => innerCache.Add(item)); }
        public void Clear() { Write(() => innerCache.Clear()); }
        public bool Contains(T item) { return ReadReturn(() => innerCache.Contains(item)); }
        public int Count { get { return ReadReturn(() => innerCache.Count); } }
        public bool IsReadOnly { get { return ReadReturn(() => innerCache.IsReadOnly); } }
        public void CopyTo(T[] array, int arrayIndex) { Read(() => innerCache.CopyTo(array, arrayIndex)); }
        public bool Remove(T item) { return WriteReturn(() => innerCache.Remove(item)); }
        public IEnumerator<T> GetEnumerator() { return ReadReturn(() => innerCache.GetEnumerator()); }
        IEnumerator IEnumerable.GetEnumerator() { return ReadReturn(() => (innerCache as IEnumerable).GetEnumerator()); }
    }
    
    internal class Program
    {
        private static SynchronisedList<int> list = new SynchronisedList<int>();
    
        private static void Main()
        {          
            for (int i = 0; i < 500000; i++)
            {
                var index = i;
                ThreadPool.QueueUserWorkItem((state) =>
                {
                    var threadNum = (int)state;
                    if (threadNum % 10 == 0)
                    {
                        list.Add(threadNum);
                    }
                    else
                    {
                        Console.WriteLine(list.Count);
                    }
                }, index);
            }
            Console.ReadKey();
        }
    }
    
    6 回复  |  直到 7 年前
        1
  •  7
  •   LukeH    15 年前

    你知道内置的吗 SynchronizedCollection<T> 班级?

    它使用标准 Monitor -基于锁定而不是 ReaderWriterLockSlim .您需要进行概要分析,以确定这是否会在特定的使用场景中造成显著的性能差异。

        2
  •  3
  •   Mats Fredriksson    15 年前

    这里有几个线程问题。

    1。 我认为getEnumerator函数在这里暴露了一个线程问题。它们提供了对不受锁控制的内车厢的引用。

    例如,如果有一个线程在列表上执行foreach,而另一个线程正在删除或插入元素,那么它可能会崩溃。

    解决方案是复制列表并返回新克隆列表上的枚举器。如果列表很长,那么回调将是内存问题。

    2。 contains()和indexof()函数或多或少是无用的,除非在同步列表之外有其他锁定方法。

    示例:线程A获取对象的索引,线程B插入/删除/更新该对象,线程A索引现在已过时。


    我不认为这真的是一个伟大的想法与完全同步的名单。编写一个定制的版本,而不是功能有限的版本。

    如果您只需要一个队列或堆栈,那么只需要两个或三个完全同步的必要方法来实现这个队列或堆栈。如果您需要更多的功能,请使用一个列表,让不同的线程进行同步。

        3
  •  2
  •   Michael Petrotta user3140870    12 年前

    这个类解决了所有问题,使您的列表100%线程安全。

    使用与数据库中的事务类似的作用域可以避免竞争条件。

    客户端代码

    List<T> unsafeList = ... 
    var threadSafeList = new SyncronisedList(unsafeList);
    using (threadSafeList.EnterReadScope()) {
       // all your sequential read operations are thread-safe
    }
    using (threadSafeList.EnterWriteScope()) {
       // all sequential read/write operations are thread-safe
    }
    

    类代码

    public class SyncronisedList<T> : IList<T> {
        private readonly ReaderWriterLockSlim _threadLock;
        private readonly IList<T> _internalList;
    
        public SyncronisedList() : this(new List<T>()) {
        }
    
        public SyncronisedList(IList<T> internalList) {
            _internalList = internalList;
            _threadLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        }
    
    
        private U Read<U>(Func<U> function) {
            using (EnterReadScope())
                return function();
        }
    
        private void Read(Action action) {
            using (EnterReadScope())
                action();
        }
    
        private U Write<U>(Func<U> function) {
            using (EnterWriteScope())
                return function();
        }
    
        private void Write(Action action) {
            using (EnterWriteScope())
                action();
        }
    
        public IDisposable EnterReadScope() {
            return new Scope<T>(this, false);
        }
    
        public IDisposable EnterWriteScope() {
            return new Scope<T>(this, true);
        }
    
        public T this[int index] {
            get { return Read(() => _internalList[index]); }
            set { Write(() => _internalList[index] = value); }
        }
    
        public int IndexOf(T item) { return Read(() => _internalList.IndexOf(item)); }
        public void Insert(int index, T item) { Write(() => _internalList.Insert(index, item)); }
        public void RemoveAt(int index) { Write(() => _internalList.RemoveAt(index)); }
        public void Add(T item) { Write(() => _internalList.Add(item)); }
        public void Clear() { Write(() => _internalList.Clear()); }
        public bool Contains(T item) { return Read(() => _internalList.Contains(item)); }
        public int Count { get { return Read(() => _internalList.Count); } }
        public bool IsReadOnly { get { return Read(() => _internalList.IsReadOnly); } }
        public void CopyTo(T[] array, int arrayIndex) { Read(() => _internalList.CopyTo(array, arrayIndex)); }
        public bool Remove(T item) { return Write(() => _internalList.Remove(item)); }
        public IEnumerator<T> GetEnumerator() { return Read(() => _internalList.GetEnumerator()); }
        IEnumerator IEnumerable.GetEnumerator() { return Read(() => (_internalList as IEnumerable).GetEnumerator()); }
    
    
        private class Scope<U> : IDisposable {
            private readonly SyncronisedList<U> _owner;
            private readonly bool _write;
    
            internal Scope(SyncronisedList<U> owner, bool write) {
                _owner = owner;
                _write = write;
                if (_write)
                    _owner._threadLock.EnterWriteLock();
                else
                    _owner._threadLock.EnterReadLock();
            }
    
            public void Dispose() {
                if (_write)
                    _owner._threadLock.ExitWriteLock();
                else
                    _owner._threadLock.ExitReadLock();
            }
    
        }
    
    }
    
        4
  •  1
  •   Guillaume    15 年前

    您的实现还可以,但您仍然需要关注同步问题:

    给出一个列表“foo”

    int index = list.IndexOf("foo");
    Console.WriteLine(list[index]);
    

    现在,如果另一个线程在这两行之间执行list.clear()呢? 您的读写器锁应该可以公开访问以处理这些情况。 当然,对于枚举器也是如此……

        5
  •  0
  •   Ian Ringrose    15 年前

    努力列一个清单 所有的人都是安全的 很难。

    我想你应该看看 操作 那个 您的应用程序需要 然后设计一个以线程安全方式公开它们的类。(列一个太低的等级)

    您的设计在现实生活中不太可能是线程安全的,因为调用列表的代码可能以不安全的方式组合操作。

    只公开一个枚举器就打开了一个 很多问题“它意味着什么?” 访问列表中的所有项目 另一个线程正在更改列表?

        6
  •  0
  •   supercat    11 年前

    对于其他操作,列表中的某些操作不能是有意义的线程安全操作。例如,一个线程将要写入元素5,而另一个线程将删除元素3,以便将原来的元素5移动到4,将原来的元素6移动到5,第一个线程将最终覆盖一个不是它所期望的元素。

    如果对其行为进行某些限制,并让它实现一个相同的 IList<T> 数组也是如此。

    我能想到的最有用的线程安全列表样式将支持以下操作,除了那些可以在实现的数组上使用的操作之外 ILIST & T;T & GT; :

    1. int add(t item)--返回新项的索引
    2. accessitem(index,actionboref proc)--使用list item作为“ref”参数的调用过程
    3. accessitem(index,actionByRef proc,ref xp1 xparam1)--调用proc,其中list item作为一个“ref”参数,xparam1作为第二个参数
    4. accessitem(index,actionByRef proc,ref xp1 xparam1,ref xp2 xparam2)--如上所述,带有另一个pass-through`ref'参数
    5. 枚举——返回在枚举开始之前创建的每个项的“活动”状态。

    列表将支持添加项,但不支持插入或删除项。自从 Add 方法将返回新创建项的索引,无论线程添加了什么项,都将对其拥有独占控制权。此外, AccessItem 基元将允许两个或多个线程以他们认为合适的方式在列表项上使用原子基元(如果 T 是具有公开字段的结构类型,原子原语也可以用于这些字段)。

    这种类型不应该建立在 List<T> ,但在 T[32][] arr . 通过设置开始 arr[0] 到A T[16] ;如果添加了第17项,则初始化 arr[1] 到A T[32] ;如果已满,则初始化 arr[2] 到A T[64] 等等。任何给定的索引都将始终由相同的数组元素表示,即使列表展开,因此对现有列表元素的访问不会受到列表展开的影响。

    一个只附加的列表可能是一个有用的线程安全类型,但是我还不知道.NET的任何版本中有任何这样的类型。