【问题标题】:Fix-sized Concurrent list固定大小的并发列表
【发布时间】:2015-05-11 15:47:54
【问题描述】:

我已经实现了一个简单的任务来创建一个固定大小的列表,该列表允许并发写入,并且可以随时转储列表中项目的最新快照。

这是我的实现。每个线程的偏移量将自动增加,如果达到列表的大小,则重置。不同的线程应该对数组的每个部分具有独立的访问权限。

我的问题是当我调用 Dump() 时,前几个项目没有存储在列表中。另外,是否有一个可以同时进行原子增加和重置的互锁函数,所以我不必创建一个更衣室对象和一个锁块?谢谢。

public static void Main(string[] args)
{            
    ConcurrentCircularFixedList<int> list = new ConcurrentCircularFixedList<int>(20);
    Enumerable.Range(1, 30).AsParallel().Select(nu => list.Enqueu(nu)).ToList();    
}

public class ConcurrentCircularFixedList<T>
{
    private int _size;
    private int _offset;
    private sealed object _locker = new Object();
    privateT[] _list;

    public ConcurrentCircularFixedList(int size)
    {
        _size = size;
        _offset = 0;
        _list = new T[_size];
    }

    public int Enqueu(T item)
    {
        _list[_offset] = item;
        lock(_locker)
        {
            Debug.Write("B " + _offset);
            _offset += 1;
            if(_offset == _size)
                _offset = 0;
            Debug.Write("A " + _offset + "\n");
         }  
         return _offset;
    }

    public T[] Dump()
    {
        return _list.ToArray();
    }
}

【问题讨论】:

  • Dump 上没有锁,但我不认为任何一个锁都是必要的。
  • 我投票结束这个问题,因为它属于 CR。
  • @BrianRasmussen,你们很快,我误按了提交按钮。请查看我的问题。
  • @Aron,在我的 Dump 方法中,我调用了 ToArray(),它应该创建一个新数组,所以我认为不需要锁。
  • @Aron Sriram 展示的示例抛出,原因是他给出的。他描述了一种可能的情况,在这种情况下,将使用数组边界之外的索引来调用索引器。更不用说物品掉在地板上或退回两次的可能性,使用此代码非常合理。

标签: c# list concurrency


【解决方案1】:

这是一个小版本的无锁列表,可在写入时复制。使用前应清楚了解其性能特点。当您有很多作家或列表很大时,它会很昂贵。由于列表实际上是不可变的,因此读取无需同步。当然,这可以通过各种方式改进,但你明白了。实际上,它牺牲了一些内存压力和较慢的写入速度来实现零成本读取。

public class CopyWriteList<T>
{
    private volatile List<T> list;

    public CopyWriteList()
    {
        list = new List<T>();
    }

    public CopyWriteList(int capacity)
    {
        list = new List<T>(capacity);
    }

    public T this[int index]
    {
        get { return list[index]; }
        set { Replace(x => x[index] = value); }
    }

    public void Clear()
    {
        Replace(x => x.Clear());
    }

    public void Add(T item)
    {
        Replace(x => x.Add(item));
    }

    //Etc....

    private void Replace(Action<List<T>> action)
    {
        List<T> current;
        List<T> updated;
        do
        {
            current = list;
            updated = new List<T>(current);
            action(updated);
        } while (Interlocked.CompareExchange(ref list, updated, current) != current);
    }

    public List<T> GetSnapshot()
    {
        return list;
    }
}

或者,这是您的代码的固定版本。请注意,读者和作者之间存在额外的争用。性能可能会因此受到影响(例如昂贵的上下文切换)。

public class ConcurrentCircularFixedList<T>
{
    private readonly int _size;
    private int _offset;
    private readonly object _locker = new Object();
    private readonly T[] _list;

    public ConcurrentCircularFixedList(int size)
    {
        _size = size;
        _offset = 0;
        _list = new T[_size];
    }

    public int Enqueue(T item)
    {
        lock (_locker)
        {
            _list[_offset] = item;
            Debug.Write("B " + _offset);
            _offset += 1;
            if (_offset == _size)
                _offset = 0;
            Debug.Write("A " + _offset + "\n");
            return _offset;
        }
    }

    public T[] Dump()
    {
        lock (_locker)
            return _list.ToArray();
    }
}

【讨论】:

  • 感谢您的解决方案。在 Interlocked.CompareExchange(ref list, updated, current) 中,为什么 list 和 current 不同?以及使列表易变的目的是什么?
  • @Helic listcurrent 可能因为多线程而不同。想象一下许多线程一次“修改”CopyWriteList。在大多数情况下,它们是相同的,CAS 会成功。 Volatile 本质上是一个提示,告诉编译器不要为单线程使用优化变量。
  • 但是 List 是引用类型,当前和列表是否应该存储指向同一个堆位置的相同引用?
  • @Helic 我不明白你在问什么。如果您不了解CopyWriteList 的代码,我建议您不要使用它。坚持使用固定版本的代码。
  • 我认为这段代码 (List current = list) 在堆栈上创建“当前”变量以及指向堆中 List 对象的“列表”变量。因此,“current”和“list”变量的值应该始终与堆上 List 对象的引用(比如说 XXX)相同。并且 Interlocked.CompareExchange(ref list, updated, current) 比较 list 和 current 变量的值,它们都是引用 XXX?
猜你喜欢
  • 1970-01-01
  • 2011-08-16
  • 2015-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多