C# 如何在.NET中写入写入列表上的副本
如何在.NET中使用写时复制模型编写线程安全列表 下面是我当前的实现,但在阅读了大量关于线程、内存障碍等的内容后,我知道在涉及到无锁的多线程时,我需要谨慎。有人能评论一下这是否是正确的实现方式吗C# 如何在.NET中写入写入列表上的副本,c#,.net,multithreading,lock-free,copy-on-write,C#,.net,Multithreading,Lock Free,Copy On Write,如何在.NET中使用写时复制模型编写线程安全列表 下面是我当前的实现,但在阅读了大量关于线程、内存障碍等的内容后,我知道在涉及到无锁的多线程时,我需要谨慎。有人能评论一下这是否是正确的实现方式吗 class CopyOnWriteList { private List<string> list = new List<string>(); private object listLock = new object(); public void Ad
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];
}
}
类CopyOnWriteList
{
私有列表=新列表();
私有对象listLock=新对象();
公共无效添加(字符串项)
{
锁(列表锁)
{
列表=新列表(列表){item};
}
}
公共无效删除(字符串项)
{
锁(列表锁)
{
var tmpList=新列表(列表);
tmpList.移除(项目);
列表=tmpList;
}
}
公共布尔包含(字符串项)
{
返回列表。包含(项目);
}
公共字符串Get(int索引)
{
返回列表[索引];
}
}
编辑
更具体地说:以上代码是线程安全的,还是应该添加更多内容?另外,所有线程最终会在列表中看到更改吗?或者我应该在列表字段或线程中添加volatile
关键字。MemoryBarrier在访问引用和调用引用方法之间包含方法
举个例子,看起来像我上面的代码,但这种方法在.NET中也是线程安全的吗
这是同样的问题,但在Java中也是如此
是与此相关的另一个问题。实现是正确的,因为引用分配是原子的,符合。我想将volatile
添加到列表中
您的方法看起来是正确的,但我建议使用字符串[]
而不是列表
来保存数据。添加项时,您确切地知道结果集合中将包含多少项,因此可以创建一个新数组,其大小与所需的大小完全相同。删除项目时,您可以获取列表
参考的副本,并在制作副本之前搜索您的项目;如果发现该项不存在,则无需删除它。如果确实存在,则可以创建一个新数组,该数组的大小与所需的大小完全相同,并将要删除的项之前或之后的所有项复制到新数组中
另一件你可能要考虑的事情是使用<代码> int [1 ] < /Cord>作为你的锁定标志,并使用一个类似于:
的模式。
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;
}
如果没有写入争用,则比较交换
将成功,而无需获取锁。如果存在写争用,写操作将由锁序列化。请注意,这里的锁既不必要也不足以确保正确性。其目的是避免在发生写争用时发生抖动。线程#1可能会通过它的第一个“if”测试,并在许多其他线程同时尝试写入列表并开始使用锁时将任务切换出去。如果出现这种情况,线程#1可能会通过执行自己的比较交换
来“意外”锁定中的线程。这样的操作将导致持有锁的线程不得不浪费时间创建一个新的数组,但这种情况应该很少发生,以至于额外数组拷贝的偶然成本应该无关紧要。是的,它是线程安全的:
添加
和删除
中的集合修改是在单独的集合上完成的,因此它避免了从添加
和删除
或从添加
/删除
和包含
/获取
同时访问同一集合
新集合的分配是在lock
内部完成的,这只是一对监视器。输入和Monitor.Exit
,这两个监视器都会像前面提到的那样设置一个完整的内存屏障,这意味着锁定后所有线程都应该观察列表
字段的新值
正确性在于使用,无需重新发明轮子。考虑一下这个问题:@Cuong Le:根据MSDN,ConcurrentBag“针对同一线程同时产生和消耗数据的场景进行了优化”。我需要对罕见的写操作和频繁的读操作进行优化,一次写一次就可以做到这一点。@Alvin Wong:这不是我需要的。1) 我不想使用F#libs 2)immutable collection只是“写时拷贝”的一部分,谢谢你的回答,但我想实现的是从列表中无锁读取。我目前使用ReaderWriterLockSlim,但希望学习如何在没有它的情况下使用“写时复制”模式。谢谢,现在的问题是:是否需要“volatile”使所有线程在“list”引用中看到更改?请查看MSDN中的volatile
。它说,声明为volatile的字段不受编译器优化的影响,编译器优化假定由单个线程访问。这可确保字段中始终存在最新的值。
即读和写保证按其显示顺序进行,读\写保证为原子。你需要它吗是的,我看了“写时复制”,因为在我的用例中,从列表中添加和删除是很少的(比如一个月一次),但检查项目是否存在是非常频繁的(一秒钟多次)。我不关心在Add和Remove中锁定的优化,我需要Contains方法的性能。“写时复制”保证了这一点,但我不确定我上面的实现是否是.NET内存模型中的线程保存。@RafałKłys:您的代码处于一个非常令人讨厌的灰色区域,因为大多数.NET实现都是在x86内存模型上运行的,而x86内存模型在实现方面比.NET内存模型更严格(用户方面更宽容)。为了完全安全,可能需要在tmpList.Remove
和list=tmpList
之间添加一个显式内存屏障(我认为System.Threading.Thread.MemoryBarrier()
);我很确定在这种情况下是不需要的