【问题标题】:Thread safety of List<T> with One Writer, No EnumeratorsList<T> 的线程安全,只有一个写入器,没有枚举器
【发布时间】:2011-07-31 20:38:18
【问题描述】:

在浏览一些 数据库 代码以寻找与此问题无关的错误时,我注意到在某些地方 List&lt;T&gt; 的使用不当。具体来说:

  1. 有许多线程同时访问List 作为读者,但使用索引 进入list 而不是enumerators
  2. list 有一个作者。
  3. 同步为零,读取器和写入器同时访问list,但由于代码结构的原因,last 元素直到执行 Add() 的方法返回。
  4. 从未从list 中删除任何元素。

根据 C# 文档,这不应该是线程安全的。 然而它从未失败过。我想知道,由于 List 的具体实现(我在内部假设它是一个 array,当它用完空间时 re-allocs),它1-writer 0-enumerator n-reader add-only 场景意外线程安全,或者在当前 .NET4 实现中是否存在一些不太可能发生的情况?

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

【问题讨论】:

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


    【解决方案1】:

    这会而且会爆炸。只是还没有。陈旧的索引通常是第一件事。它会在你不想吹的时候吹。你现在可能很幸运。

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

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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 列表的线程安全与列表中对象字段的线程安全没有任何关系。即使您使实际列表线程安全,您所描述的问题仍然存在。
      • @Brois 是的,你是对的,他应该锁定所有可以从多线程更新的东西。
      【解决方案3】:

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

      【讨论】:

      • 我同意,尤其是在 OP 对读者的只读访问权限进行了编辑之后。
      • -1。这是错误的:“即使有人在写,而有人在读,读者要么得到旧数据,要么得到新数据,它仍然有效。” 它是否有效是该特定的实现细节数据结构,除非有保证,否则您不能依赖它。 List&lt;T&gt; documentation 声明它可以以线程安全的方式支持多读取器-无写入器。到目前为止,OP 可能很幸运,拥有多位读者和单位作者,但不能保证这种情况会继续存在。
      • 我认为这个问题已经得到解决,因为他使用的唯一写入是 Add 并且只有在 add 返回后才能访问新元素。请记住,他使用的是一些自定义列表实现,而不是 .NET 框架中的 List 版本。
      • @MrFox:List&lt;T&gt; 有可能以这样一种方式实现,甚至不是非常困难,即使不使用任何锁,也可以在单个线程运行时安全地读取或枚举列表执行Add 操作或索引写入(但不是InsertRemove 或类似的东西)。然而,规范中没有任何内容表明它实际上是以这种方式实现的;至少在枚举方面,显然不是。
      【解决方案4】:

      那么,如果架构是 32 位,写入大于 32 位的字段,例如 long 和 double,就不是线程安全的操作;请参阅System.Double 的文档:

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

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

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

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

      【讨论】:

      • 对于 32 位系统上的 32 位类型,该列表不是线程安全的,因为向列表中添加一个项目会写入多个值。至少它必须写入新项目并更新列表大小 - 一旦您在没有锁的情况下执行多个操作,就没有线程安全性。更不用说当列表重新调整大小时会发生什么。
      【解决方案5】:

      首先,对于一些帖子和 cmets,文档何时可靠?

      其次,这个答案更多的是针对一般问题而不是 OP 的具体问题。

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

      1. List 类是否实现为平面数组?

      如果是,那么:

      1. 能否在写入过程中抢占写入指令>

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

      因此,如果您通过为某个指针提供偏移量来索引数组,则无需线程锁定即可安全读取。如果 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
      

      即使有内存分配,这也比锁定列表并在解锁之前迭代整个事物要快。

      请注意:如果您想要一个快速的系统,线程锁定是您最大的敌人。请改用ZeroMQ。我可以根据经验说话,基于消息的同步是正确的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-12
        • 2014-09-09
        • 1970-01-01
        • 2011-05-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多