我正在使用C#Framework 2.0
我有一个对象的arraylist。我需要使它对于单线程读取和另一单线程编写器场景来说是线程安全的。
读取每秒发生数百次,而写入(涉及删除或添加数组中的元素)的操作很少(如果有的话)大概每周从UI发生一次。
我总是可以使用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来说,这将是非常可怕的,因为它使写入比必须的要昂贵得多。但是,正如您所说的那样,读取频率比读取频率低60,000,000倍,因此在这种情况下,保持平衡是有意义的。
使用这种方法的安全阅读注意事项:
由于考虑了类似的案例,我做出了错误的假设。
以下是安全的:
for(object obj in al)
DoSomething(obj);
即使
DoSomething
可能花费很长时间。原因是,这一次调用了GetEnumerator()
,然后在一个枚举器上工作,即使此期间al
发生了变化,枚举器仍将与同一列表保持联系(这是陈旧的,但很安全)。以下是不安全的:
for(int i = 0; i < al.Count; ++i)
DoSomething(al[i]);//
Count
是安全的,但是到al[i]
时它可能已经过时,这意味着当al[43]
只有20个项目时,您会调用al
。调用
BinarySearch
,Clone
,Contains
,IndexOf
和GetRange
也是安全的,但是使用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
所做的一切都是安全的,因为如果它变得陈旧,它仍然是旧列表的一个线程自己的版本,并且不会出错。所以。要么在
foreach
中做所有事情(我错误地假设是这种情况),要么将一次调用的结果返回到上面提到的安全方法(但不要用它来决定是否对al
进行其他操作),或者分配al
到局部变量并对其进行操作。另一个确实是重要警告,是写程序线程是否实际上将对数组列表中包含的任何对象设置任何属性或调用任何非线程安全的方法。如果确实如此,那么您已经从一个线程同步问题开始思考,到几十个问题了!
如果是这种情况,那么您不仅要担心数组列表,还要担心可以更改的每个对象(通过更改,我的意思是在
.Append("abc")
上调用StringBuilder
的方式会更改该对象,而不是用str = "abc"
的方式替换为全新的对象更改str
)。有四种安全的可能性:
关于c# - .NET基本线程-为单个读取器和单个写入器线程同步对象的最有效方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8494097/