如何使用.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是另一个与此相关的问题。
答案 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)
是的,它是线程安全的:
Add
和Remove
中的集合修改是在单独的集合上完成的,因此可以避免从Add
和Remove
或{来自同时访问同一集合{1}} / Add
和Remove
/ Contains
。
新集合的分配是在Get
内完成的,lock
只是一对Monitor.Enter和Monitor.Exit
,它们都按照标记here执行完整的内存屏障,这意味着在锁定之后,所有线程都应该观察list
字段的新值。