如何在.NET中编写copy-on-write列表

时间:2013-06-28 13:56:38

标签: c# .net multithreading lock-free copy-on-write

如何使用.NET中的copy-on-write模型编写线程安全列表?

下面是我目前的实现,但经过大量关于线程,内存障碍等的阅读后,我知道在涉及无锁的多线程时我需要谨慎。如果这是正确的实现,有人会评论吗?

class CopyOnWriteList
{
    private List<string> list = new List<string>();

    private object listLock = new object();

    public void Add(string item)
    {
        lock (listLock)
        {
            list = new List<string>(list) { item };
        }
    }

    public void Remove(string item)
    {
        lock (listLock)
        {
            var tmpList = new List<string>(list);
            tmpList.Remove(item);
            list = tmpList;
        }
    }

    public bool Contains(string item)
    {
        return list.Contains(item);
    }

    public string Get(int index)
    {
        return list[index];
    }
}

修改

更具体一点:代码线程是否安全,还是应该添加更多内容?此外,所有线程最终都会看到list引用中的更改吗?或者我可以在列表字段中添加volatile关键字,或者在包含访问引用和调用方法的方法中添加Thread.MemoryBarrier吗?

这是例如Java implementation,看起来像我上面的代码,但这种方法在.NET中也是线程安全的吗?

here是同一个问题,但也是Java。

Here是另一个与此相关的问题。

3 个答案:

答案 0 :(得分:0)

实现是正确的,因为引用分配是根据Atomicity of variable references原子的。我会将volatile添加到list

答案 1 :(得分:0)

您的方法看似正确,但我建议使用string[]而不是List<string>来保存您的数据。当您添加项目时,您确切知道结果集合中将有多少项目,因此您可以创建一个完全符合所需大小的新数组。删除项目时,您可以获取list参考的副本,并在复制前搜索您的项目;如果事实证明该项目不存在,则无需将其删除。如果确实存在,则可以创建具有所需大小的新数组,并将要删除的项目之前或之后的所有项目复制到新数组。

您可能想要考虑的另一件事是使用int[1]作为锁定标志,并使用类似的模式:

static string[] withAddedItem(string[] oldList, string dat)
{
  string[] result = new string[oldList.Length+1];      
  Array.Copy(oldList, result, oldList.Length);
  return result;
}
int Add(string dat) // Returns index of newly-added item
{
  string[] oldList, newList;
  if (listLock[0] == 0)
  {
    oldList  = list;
    newList = withAddedItem(oldList, dat);
    if (System.Threading.Interlocked.CompareExchange(list, newList, oldList) == oldList)
      return newList.Length;
  }
  System.Threading.Interlocked.Increment(listLock[0]);
  lock (listLock)
  {
    do
    {
      oldList  = list;
      newList = withAddedItem(oldList, dat);
    } while (System.Threading.Interlocked.CompareExchange(list, newList, oldList) != oldList);
  }
  System.Threading.Interlocked.Decrement(listLock[0]);
  return newList.Length;
}

如果没有写入争用,CompareExchange将成功而无需获取锁定。如果存在写入争用,则锁定将对序列进行序列化。请注意,此处的锁定既不必要也不足以确保正确性。其目的是避免在写入争用时发生颠簸。线程#1可能会超过其第一个“if”测试,并且任务任务切换出来,而许多其他线程同时尝试编写列表并开始使用锁定。如果发生这种情况,那么线程#1可能会通过执行自己的CompareExchange来“惊吓”锁中的线程。这样的操作会导致lock - 持有线程不得不浪费时间创建一个新数组,但这种情况应该很少出现,以至于额外数组副本的偶然成本无关紧要。

答案 2 :(得分:0)

是的,它是线程安全的:

  1. AddRemove中的集合修改是在单独的集合上完成的,因此可以避免从AddRemove或{来自同时访问同一集合{1}} / AddRemove / Contains

  2. 新集合的分配是在Get内完成的,lock只是一对Monitor.Enter和Monitor.Exit,它们都按照标记here执行完整的内存屏障,这意味着在锁定之后,所有线程都应该观察list字段的新值。