C# 基本.NET线程-为单个读写器线程同步对象的最有效方法
我使用的是c#framework 2.0 我有一个对象的数组列表。我需要使它对于单线程读取和另一个单线程编写器场景是线程安全的。 读操作每秒发生几百次,而写操作(包括删除或添加数组中的元素)很少发生,如果有的话,可能每周从UI中发生一次C# 基本.NET线程-为单个读写器线程同步对象的最有效方法,c#,multithreading,performance,.net-2.0,C#,Multithreading,Performance,.net 2.0,我使用的是c#framework 2.0 我有一个对象的数组列表。我需要使它对于单线程读取和另一个单线程编写器场景是线程安全的。 读操作每秒发生几百次,而写操作(包括删除或添加数组中的元素)很少发生,如果有的话,可能每周从UI中发生一次 我可以始终使用lock(),但如何使此阵列线程安全,同时使读卡器线程的性能和延迟开销最小。由于只有读卡器和一个写入器,因此在这种情况下,ReaderWriterLock没有帮助。在这种情况下,经典的锁可能是最好的选择 话虽如此,由于读者需要经常阅读,因此经常检查
我可以始终使用lock(),但如何使此阵列线程安全,同时使读卡器线程的性能和延迟开销最小。由于只有读卡器和一个写入器,因此在这种情况下,
ReaderWriterLock
没有帮助。在这种情况下,经典的锁可能是最好的选择
话虽如此,由于读者需要经常阅读,因此经常检查锁,一个健壮的解决方案是自旋锁:
编辑:事实上,正如David Silva所指出的,自旋锁
在.net 2.0中不可用。但是,它可以通过重复调用Interlocated.CompareExchange来实现,但在这个阶段,可能不需要详细讨论。经典的锁是您最好的选择。因为只有一个读卡器和一个写卡器,所以在这种情况下,读写器锁将不起作用。在这种情况下,经典的锁可能是最好的选择
话虽如此,由于读者需要经常阅读,因此经常检查锁,一个健壮的解决方案是自旋锁:
编辑:事实上,正如David Silva所指出的,自旋锁
在.net 2.0中不可用。但是,它可以通过重复调用Interlocated.CompareExchange来实现,但在这个阶段,可能不需要详细讨论。经典的锁
是您最好的选择。我只会使用锁
。如果它是无争用的,它的速度非常快——当然可以达到毫秒周期。我会使用锁。如果它是无争用的,它的速度非常快——当然可以达到毫秒周期。我最近经历了这个过程。事实证明,单生产者/消费者队列的无锁版本的性能增益几乎没有意义(每个队列/出列大约0.0002毫秒)
我的建议是只使用lock()语句,它可以工作。你也可以看看我写的无锁版本,见。本系列的最后一篇文章提供了一个您也可以使用的方法。我最近经历了这个过程。事实证明,单生产者/消费者队列的无锁版本的性能增益几乎没有意义(每个队列/出列大约0.0002毫秒)
我的建议是只使用lock()语句,它可以工作。你也可以看看我写的无锁版本,见。本系列的最后一篇文章有一个您也可以使用的主题。编辑:任何人遇到这个问题,我鼓励您看看讨论。读者线程修改数组列表中的对象这一点对于这类关于共享集合的问题非常重要(以及为什么在这种情况下我可能会选择“对整个事件进行大锁定”而不是我在这里给出的方法,没有对这些修改进行详细分析。尽管如此,以下内容仍然适用于某些情况:
对于您描述的从读到写的模式,我将执行以下操作
假设ArrayList被称为al
。对于所有读取,我(大部分情况下,请参见下文)都会忽略所有线程安全性(不要担心,我的疯狂中有方法)。要编写,我会:
ArrayList temp = new ArrayList(al);//create copy of al
/* code to update temp goes here */
al = temp;//atomic overwrite of al.
Thread.MemoryBarrier();//make sure next read of al isn't moved by compiler or from stale cache value.
复制al
是安全的,因为ArrayList对于多个读卡器是安全的。分配是安全的,因为我们正在覆盖引用,引用是原子的。我们只需要确保存在内存障碍,尽管在大多数当前系统中,我们可能会跳过它(但不要这样做,至少理论上是必要的,我们不要再猜测了)
对于大多数共享使用arraylist的人来说,这是非常可怕的,因为这样做会使写操作比必须的要昂贵得多。但是,正如您所说,这些操作的频率比读操作的频率低60000000倍,因此在这种情况下,平衡是有意义的
关于这种方法的安全阅读注意事项:
由于考虑到一个类似的案例,我做出了一个错误的假设。
以下是安全的:
for(object obj in al)
DoSomething(obj);
即使是DoSomething
也可能需要很长时间。原因是,这会调用GetEnumerator()
一次,然后在一个枚举器上工作,该枚举器将保持与同一列表的关联,即使al
同时发生更改(它将过时,但安全)
以下情况不安全:
for(int i = 0; i < al.Count; ++i)
DoSomething(al[i]);//
注意,我们没有复制al
,只是复制引用,所以这真的很便宜。我们对list
所做的一切都是安全的,因为如果它过时了,它仍然是旧列表的一个线程版本,不会出错
因此,要么在foreach
中执行所有操作(我错误地假设了这种情况),返回对上述安全方法的单个调用的结果(但不要使用它来决定是否对al
执行另一个操作),要么将al
分配给局部变量并对其执行操作
另一个真正重要的警告是,writer线程是否真的会在arraylist中包含的任何对象上设置任何属性或调用任何非线程安全方法。如果是这样,那么您就从只考虑一个线程同步问题变成了几十个
如果是这种情况,那么您不仅要担心arraylist,还要担心每个可以更改的对象(所谓更改,是指在StringBuilder上调用.Append(“abc”)
更改该对象,而不是以str=“abc”
changesstr的方式使用全新的对象替换
ArrayList list = al;
for(int i = 0; i != list.Count; ++i)
DoSomething(list[i]);
DoSomethingElse(list);
DoSomethingReallyComplicated(list);
for(int i = 0; i != list.Count; ++i)
SureWhyNotLetsLoopAgain(list[i]);