如何在 .NET 中编写写入时复制列表

本文关键字:复制 列表 NET | 更新日期: 2023-09-27 18:20:13

如何在 .NET 中使用写入时复制模型编写线程安全列表?

下面是我当前的实现,但是在阅读了大量有关线程,内存障碍等的内容后,我知道在涉及没有锁的多线程时需要谨慎。有人可以评论这是否是正确的实现吗?

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更改吗?或者,也许我应该在列表字段或包含方法中的 Thread.MemoryBarrier 中添加volatile关键字,在访问引用和调用方法之间?

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

这是同样的问题,但在Java中也是如此。

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

如何在 .NET 中编写写入时复制列表

实现是正确的,因为根据变量引用的原子性,引用赋值是原子的。我会在list中添加volatile.

您的方法看起来是正确的,但我建议使用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保持线程不得不浪费时间制作新数组,但这种情况应该很少出现,以至于额外数组副本的偶尔成本应该无关紧要。

是的,它是线程安全的:

  1. AddRemove中的集合修改是在单独的集合上完成的,因此它避免了从AddRemove或从Add/RemoveContains/Get并发访问同一集合。

  2. 新集合的分配是在 lock 内部完成的,它只是一对 Monitor.Enter 和 Monitor.Exit ,它们都做一个完整的内存屏障,如此处所述,这意味着在锁定后所有线程都应该观察 list 字段的新值。