<T> 具有一个编写器的列表的线程安全性,没有枚举器



在通过一些数据库代码寻找与此问题无关的bug时,我注意到在某些地方List<T>被不恰当地使用。具体来说:

  1. 有多个线程同时作为读线程访问List,但在list中使用索引而不是enumerators
  2. list只有一个写入器。
  3. 没有同步,读写器同时访问list,但是由于代码结构的原因,在执行Add()的方法返回之前,最后一个元素永远不会被访问。
  4. 未从list中删除任何元素。
根据 c# 文档,这应该不是线程安全的。然而,它从未失败过。我想知道,由于List的特定实现(我在内部假设它是一个数组在空间耗尽时重新分配),如果1-writer 0-enumerator n-reader仅添加场景意外线程安全,或者有一些不太可能的场景,这可能会在当前中爆发。NET4 实现吗?重要的细节我在阅读一些回复时漏掉了。阅读器将List及其内容视为只读的。

这会爆炸。只是还没有。过时的索引通常是最先被淘汰的。它会在你不希望的时候爆炸。你现在可能很幸运。

当你使用。net 4.0时,我建议将列表从System.Collections.Concurrent更改为合适的集合,以保证线程安全。我也会避免使用数组索引和切换到ConcurrentDictionary,如果你需要查找的东西:

http://msdn.microsoft.com/en-us/library/dd287108.aspx

因为它从来没有失败过或者你的应用程序没有崩溃,这并不意味着这个场景是线程安全的。例如,假设写线程更新列表中的一个字段,假设这是一个long字段,同时读线程读取该字段。返回的值可能是两个字段的按位组合,旧的和新的!这可能是因为读线程开始从内存中读取值,但在它完成读取之前,写线程刚刚更新了它。

编辑:当然,如果我们假设读者线程将只是读取所有的数据而不更新任何东西,我相信他们不会改变数组的值,但是,但是他们可以改变他们读取的值中的属性或字段。例如:

for (int index =0 ; index < list.Count; index++)
{
    MyClass myClass = list[index];//ok we are just reading the value from list
    myClass.SomeInteger++;//boom the same variable will be updated from another threads...
}

这个例子没有讨论列表本身的线程安全,而是讨论列表公开的共享变量。

结论是,在与列表交互之前,您必须使用诸如lock之类的同步机制,即使它只有一个写入器并且没有删除任何项,这将帮助您防止一开始就不必要的小错误和失败场景。

线程安全仅在数据被多次修改时才重要。读者的数量并不重要。即使有人在写的时候有人在读,读者要么得到旧数据,要么得到新数据,它仍然有效。元素只能在Add()返回后访问,这一事实防止了元素的各个部分被单独读取。如果开始使用Insert()方法,读取器可能会得到错误的数据。

如果结构是32位,那么写大于32位的字段(如long和double)不是线程安全操作;请参阅System的文档。双:

在所有硬件平台上分配此类型的实例不是线程安全的,因为该实例的二进制表示可能太大,无法在单个原子中分配操作。

如果list的大小是固定的,那么只有当list存储的值类型大于32位时才会出现这种情况。如果列表只包含引用类型,那么任何线程安全问题都源于引用类型本身,而不是它们在列表中的存储和检索。例如,与可变引用类型相比,不可变引用类型不太可能引起线程安全问题。

而且,你不能控制List的实现细节:这个类主要是为性能而设计的,将来很可能会随着性能而改变,而不是考虑线程安全。

特别是,向列表中添加元素或以其他方式更改其大小是不安全的,即使列表的元素是32位长,因为插入、添加或删除所涉及的内容比将元素放入列表中更多。如果在其他线程访问列表之后需要这样的操作,那么锁定对列表的访问或使用并发列表实现是更好的选择。

首先,对于一些帖子和评论,从什么时候开始文档可靠了?

第二,这个答案更多的是一般性问题,而不是op的具体细节。

理论上我同意fox先生的观点,因为这一切可以归结为两个问题:

  1. 列表类是作为平面数组实现的吗?

如果是,则:

  1. 写指令可以在写过程中被抢占吗>

我相信不是这样的——在任何东西可以读取DWORD或其他东西之前,完整的写入将发生。换句话说,永远不会发生这样的情况:我写入DWORD的四个字节中的两个,然后你读取新值的1/2和旧值的1/2。

所以,如果你通过提供一个指针的偏移量来索引一个数组,你可以在没有线程锁定的情况下安全地读取。如果List做的不仅仅是简单的指针运算,那么它就不是线程安全的。

如果List没有使用平面数组,我想你现在已经看到它崩溃了。

我自己的经验是,在没有线程锁定的情况下,通过索引从List中读取单个元素是安全的。这只是我个人的看法,所以请接受它的价值。

最坏的情况,比如需要遍历列表,最好的做法是:

  1. 锁定列表
  2. 创建一个相同大小的数组
  3. 使用CopyTo()将List复制到数组
  4. 解锁列表
  5. 然后遍历数组而不是列表。

在(无论你称之为。net) c++中:

  List<Object^>^ objects = gcnew List<Object^>^();
  // in some reader thread:
  Monitor::Enter(objects);
  array<Object^>^ objs = gcnew array<Object^>(objects->Count);
  objects->CopyTo(objs);
  Monitor::Exit(objects);
  // use objs array

即使有内存分配,这也比锁定List并在解锁之前遍历整个List要快。

只是一个提醒:如果你想要一个快速的系统,线程锁定是你最大的敌人。使用ZeroMQ代替。从我的经验来看,基于消息的同步是正确的方法。

相关内容

  • 没有找到相关文章

最新更新