如何在 .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中也是如此。
这是与此相关的另一个问题。
实现是正确的,因为根据变量引用的原子性,引用赋值是原子的。我会在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
保持线程不得不浪费时间制作新数组,但这种情况应该很少出现,以至于额外数组副本的偶尔成本应该无关紧要。
是的,它是线程安全的:
-
Add
和Remove
中的集合修改是在单独的集合上完成的,因此它避免了从Add
和Remove
或从Add
/Remove
和Contains
/Get
并发访问同一集合。 -
新集合的分配是在
lock
内部完成的,它只是一对 Monitor.Enter 和Monitor.Exit
,它们都做一个完整的内存屏障,如此处所述,这意味着在锁定后所有线程都应该观察list
字段的新值。