我正在使用c#framework 2.0
我有一个对象的arraylist。我需要为单个线程读取和另一个单线程编写器场景使其线程安全。 读取每秒发生几百次,而写入(涉及删除或添加数组中的元素)很少发生,如果有的话,可能每周一次从UI发生。
我总是可以使用lock()但是如何以最小的性能和读取器线程的延迟开销使这个数组线程安全。
答案 0 :(得分:4)
编辑:有人遇到这个,我建议你看看讨论。有关读者线程修改数组列表中对象的要点对于关于共享集合的整个类问题非常重要(以及为什么在这种情况下我可能会选择“只是对整个事物进行大锁”而不是我在这里给出的方法,而没有对这些修改进行详细分析。但是,以下情况仍然适用于某些情况:
对于您描述的读写模式,我会执行以下操作。
假设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而言,这将是非常可怕的,因为它使写入比它们必须要昂贵得多。然而,正如你所说的那样,那些频率比读数少60,000,000倍,因此在这种情况下,这种平衡是有道理的。
使用此方法安全阅读的注意事项:
由于考虑了类似的案例,我做了一个错误的假设。 以下是安全的:
for(object obj in al)
DoSomething(obj);
即使DoSomething
可能需要很长时间。原因是,这会调用GetEnumerator()
一次,然后对枚举器进行处理,即使al
在此期间发生变化(它将过时但安全),该枚举器将保持与同一列表相关。 / p>
以下是不安全的:
for(int i = 0; i < al.Count; ++i)
DoSomething(al[i]);//
Count
是安全的,但是当您到达al[i]
时可能会过时,这可能意味着当al[43]
只有20个项目时,您可以致电al
。< / p>
拨打BinarySearch
,Clone
,Contains
,IndexOf
和GetRange
也是安全的,但使用{{1}的结果却不安全},BinarySearch
或Contains
决定您使用IndexOf
做的第二件事,因为到那时它可能会改变。
以下也是安全的:
al
请注意,我们不会复制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]);
,只是复制引用,所以这真的很便宜。我们使用al
做的所有事情都是安全的,因为如果它变得陈旧,它仍然是一个线程自己的旧列表版本,并且不会出错。
因此。要么在list
(我错误地假设的情况下)中执行所有操作,请将单个调用的结果返回到上面指出的安全方法(但不要使用它来决定是否对{{进行另一个操作) 1}}),或将foreach
分配给局部变量并对其进行操作。
另一个真正重要的警告,是作者线程是否实际设置任何属性或在arraylist中包含的任何对象上调用任何非线程安全方法。如果确实如此,那么你已经从一个线程同步问题开始思考,到了几十个!
如果是这种情况,那么您不仅要担心arraylist,而且还要担心每个可以更改的对象(通过更改我的意思是al
上调用al
的方式会更改该对象,而不是以.Append("abc")
更改StringBuilder
)的方式替换为全新的对象。
有四种安全的可能性:
答案 1 :(得分:3)
由于只有读者和一位作家,ReaderWriterLock
在这种情况下无济于事。在这种情况下,经典lock
可能是最佳选择。
话虽如此,由于读者需要经常阅读并经常检查锁定,因此一个强大的解决方案是SpinLock:http://msdn.microsoft.com/en-us/library/system.threading.spinlock.aspx
编辑:事实上,正如David Silva所指出的,SpinLock
在.net 2.0中不可用。但是,它可以通过重复调用Interlocked.CompareExchange
来实现,但在此阶段可能进入细节并不相关。经典lock
是您的最佳选择。
答案 2 :(得分:2)
我只想使用lock
。如果它是无竞争的,那么速度非常快 - 当然能够达到毫秒级。
答案 3 :(得分:1)
我最近经历了这个过程。事实证明,无锁版本的单生产者/消费者队列的性能提升几乎没有意义(每个队列/出队约0.0002毫秒)。
我的建议是只使用lock()语句,它可以工作。您也可以查看我写的无锁版本,请参阅Writing a Lockless Queue for a single producer/consumer。本系列的最后一篇文章有一个locking queue你也可以使用。