5

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

  1. 有许多线程同时访问Listas 阅读器,但使用索引list不是enumerators.
  2. 有一个作家list
  3. 同步为零,读取器和写入器同时访问list,但由于代码结构,在执行返回的方法之前永远不会访问最后一个元素。Add()
  4. 从未从list.

根据C#文档,这不应该是线程安全的。然而它从未失败过。我想知道,由于List(我在内部假设它是一个在空间不足时重新分配的数组)的特定实现,它是 1-writer 0-enumerator n-reader add-only 场景意外线程安全,还是有一些不太可能的情况在当前的.NET4实现中发生这种情况?

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

4

5 回答 5

2

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

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

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

于 2011-07-31T21:03:30.037 回答
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在与列表交互之前,即使它只有一个写入器并且没有删除任何项目,这将帮助您防止微小的错误和失败场景,您首先是可有可无的。

于 2011-07-31T20:50:46.153 回答
0

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

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

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

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

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

于 2011-07-31T21:03:37.477 回答
0

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

其次,这个答案更多的是针对一般问题而不是 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

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

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

于 2012-01-17T03:02:36.530 回答
0

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

于 2011-07-31T21:11:28.223 回答