基本.NET线程 - 为单个读取器和单个写入器线程同步对象的最有效方法

时间:2011-12-13 18:15:17

标签: c# multithreading performance .net-2.0

我正在使用c#framework 2.0

我有一个对象的arraylist。我需要为单个线程读取和另一个单线程编写器场景使其线程安全。 读取每秒发生几百次,而写入(涉及删除或添加数组中的元素)很少发生,如果有的话,可能每周一次从UI发生。

我总是可以使用lock()但是如何以最小的性能和读取器线程的延迟开销使这个数组线程安全。

4 个答案:

答案 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>

拨打BinarySearchCloneContainsIndexOfGetRange也是安全的,但使用{{1}的结果却不安全},BinarySearchContains决定您使用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. 你的arraylist中有不可变的对象 - 快乐的日子。
  2. 你有可变对象,但不要改变它们 - 也不错,但你必须确定
  3. 你有线程安全对象 - 好吧,但这确实意味着你至少有一组线程问题比这个问题处理的问题更复杂。
  4. 您可以锁定每个对象以进行写入和读取 - 虽然使用此类精确锁定进行争用的锁定的可能性较小,但这有其自身的沉重感,实际上您并不比默认锁定更好(并且应该是默认设置) “只是锁定,无论如何,锁定都很便宜。”

答案 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你也可以使用。