2011-07-31 37 views
5

虽然经历了一些数据库代码寻找无关在这个问题上的错误,我注意到,在一些地方List<T>正在使用不当。具体做法是:线程安全的,没有普查员

  1. 有许多线程同时访问List作为读者,但使用指标list,而不是enumerators
  2. list有一位作家。
  3. 有零同步,读者和作者都在同一时间访问list,但由于代码结构的最后元素永远不会直到执行Add()返回的方法来访问。
  4. list以前没有任何元素被删除。

C#文档,这不应该是线程安全的。但它从未失败过。我想知道,由于List的具体实现(我假设在内部它是一个数组当空间不足时重新分配),它是1-writer 0-enumerator n-reader的附加模块场景意外线程安全,还是有一些不太可能的情况下,这可能会炸毁在当前的实施?

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

回答

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),即使它只有一个写入程序并且没有任何项目被移除,这将有助于防止您不必担心的锡蒂错误和故障情形首先。

+1

列表的线程安全性与列表中的对象字段的线程安全性无关。即使您将实际列表设置为线程安全,您所描述的问题仍然存在。 –

+0

@Brois是的,你是对的,他应该锁定每一件可以从多线程更新的东西。 –

+0

@Chuu有没有反馈? –

2

这可以并且会打击。它还没有。陈旧的指数通常是第一件事情。它会在你不想要的时候吹。你现在可能很幸运。

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

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

0

由此推论,如果架构是32位,写场大于32位,这样的只要双重,不是一个线程安全的操作;看到文档System.Double

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

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

此外,您无法控制List的实现细节:该类主要是为性能而设计的,并且在将来可能会在此方面进行更改,而不是考虑线程安全性。

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

+0

32位系统上的32位类型的列表不是线程安全的,因为将项目添加到列表中会写入多个值。至少它必须写入新项目并更新列表大小 - 一旦您在没有锁定的情况下执行多个操作,就没有线程安全性。更不用说当名单重新规模化时会发生什么。 – shf301

+0

我明白了,谢谢你的提示。我会更新答案。 –

0

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

+0

我同意,特别是在OP的编辑关于读者的只读访问之后。 –

+0

-1。这是错误的:*“即使某人正在写某人阅读时,读者或者获得旧数据或新数据,它仍然有效。”*它是否有效是该特定数据结构的实现细节,除非它是有保证的,那么你不能依靠它。 ['列表'文档](http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx)指出它可以以线程安全的方式支持多读者 - 无编写者。迄今为止,OP可能是幸运的,拥有多个读者的单一作者,但不能保证这种情况会持续下去。 – LukeH

+0

我认为这个问题是被强调的,因为他使用的唯一的写法是Add,并且只有在添加返回后才能访问新元素。请记住,他正在使用一些自定义列表实现,而不是.NET框架中的List 版本。 – MrFox

0

首先,一些帖子和评论,因为什么时候文件可靠?

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

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

  1. 是List类实现为平板阵列?

如果是的话:

  1. 可以写入指令在写>

中间被抢占,我相信这是不是这样的 - 全写会发生在任何事情都可以读取DWORD或任何东西之前。换句话说,我永远不会发生这样的情况:我写了一个DWORD的四个字节中的两个字节,然后读取1/2的新值和1/2的旧值。

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

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

我自己的经验是,通过索引从列表中读取单个项目是安全的,无需线程锁定。这只是恕我直言,但所以要拿它的价值。

最坏的情况,例如,如果您需要在列表中迭代,做的最好的事情是:

  1. 锁定列表
  2. 创建数组大小相同
  3. 使用CopyTo从()将列表复制到阵列
  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。我可以从经验中发言,基于消息的同步是正确的路线。