具有一个Writer,无枚举器的List <t>的线程安全性</t>

时间:2011-07-31 20:38:18

标签: c# list thread-safety concurrent-programming

在查看与此问题无关的一些数据库代码时,我注意到在某些地方List<T>使用不当。具体做法是:

  1. 有许多线程同时访问List作为读者,但使用索引进入list而不是enumerators
  2. list有一位作家。
  3. 没有同步,读者和作者同时访问list,但由于代码结构, last 元素永远不会被访问直到执行Add()的方法返回。
  4. list
  5. 中删除了任何元素

    通过 C#文档,这不应该是线程安全的。 但它从未失败过。我想知道,因为List的具体实现(我假设内部是一个数组,当空间用完时重新分配),它是1-writer 0-enumerator n-reader add-only scenario 意外线程安全,或者是否有一些不太可能的情况,这可能会在当前的 .NET4 实现中爆炸?

    编辑:重要细节我遗漏了阅读一些回复。读者将List及其内容视为只读。

5 个答案:

答案 0 :(得分:2)

这可以而且将会受到打击。它还没有。过时的指数通常是第一件事。只要你不想要它就会爆炸。你现在可能很幸运。

当你使用.Net 4.0时,我建议将列表从System.Collections.Concurrent更改为合适的集合,这保证是线程安全的。如果你需要查找一些内容,我也会避免使用数组索引并切换到ConcurrentDictionary:

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

答案 1 :(得分:1)

由于它从未失败或您的应用程序没有崩溃,这并不意味着此方案是线程安全的。例如,假设编写者线程确实更新了列表中的一个字段,假设这是一个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之类的同步机制,即使它只有一个编写器而且没有删除任何项目,这将有助于防止出现小错误和故障情况首先是可有可无的。

答案 2 :(得分:0)

接下来,如果架构为32位,则写入大于32位的字段(例如long和double)不是线程安全操作;请参阅System.Double的文档:

  

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

但是,如果列表的大小是固定的,则只有当List存储大于32位的值时,这种情况才有意义。如果列表仅包含引用类型,则任何线程安全问题都源于引用类型本身,而不是来自它们的存储和从List中检索。例如,不可变引用类型比可变引用类型更不可能引起线程安全问题。

此外,您无法控制List的实现细节:该类主要是为性能而设计的,并且考虑到该方面,它可能会在未来发生变化,而不是考虑线程安全性。

特别是,即使列表的元素长度为32位,向列表中添加元素或以其他方式更改其大小也不是线程安全的,因为插入,添加或删除更多内容而不仅仅是将元素放在列表中。如果在其他线程访问列表后需要此类操作,则锁定对列表的访问或使用并发列表实现是更好的选择。

答案 3 :(得分:0)

线程安全仅在数据被一次修改多次时才有意义。读者人数无关紧要。即使有人在有人读书时写作,读者要么获得旧数据,要么获得旧数据,它仍然有效。只有在Add()返回后才能访问元素这一事实可以防止元素的某些部分被单独读取。如果你开始使用Insert()方法,读者可能会得到错误的数据。

答案 4 :(得分:0)

首先,关于一些帖子和评论,因为文档何时可靠?

其次,这个答案更多的是一般性问题,而不是OP的细节。

我在理论上同意MrFox,因为这一切归结为两个问题:

  1. List类是否实现为平面数组?
  2. 如果是,那么:

    1. 写入指令可以在写入&gt;
    2. 的中间被抢占

      我相信情况并非如此 - 完全写入将在任何事物可以读取DWORD或其他之前发生。换句话说,我永远不会发生写入DWORD的四个字节中的两个,然后读取新值的1/2和旧值的1/2。

      因此,如果您通过为某个指针提供偏移量来索引数组,则可以安全地读取而不进行线程锁定。如果List不仅仅是简单的指针数学,那么它就不是线程安全的。

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

      我自己的经验是,通过索引从List中读取单个项目而不进行线程锁定是安全的。这只是恕我直言,所以把它当作它的价值。

      最糟糕的情况是,如果您需要遍历列表,最好的办法是:

      1. 锁定列表
      2. 创建一个大小相同的数组
      3. 使用CopyTo()将List复制到数组
      4. 解锁列表
      5. 然后遍历数组而不是列表。
      6. in(无论你怎么称呼.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并在解锁之前迭代整个内容更快。

        尽管如此:如果你想要一个快速的系统,螺纹锁定是你最大的敌人。请改用ZeroMQ。我可以从经验中说出,基于消息的同步是正确的方法。