在通过一些数据库代码寻找与此问题无关的bug时,我注意到在某些地方List<T>
被不恰当地使用。具体来说:
- 有多个线程同时作为读线程访问
List
,但在list
中使用索引而不是enumerators
。 -
list
只有一个写入器。 - 没有同步,读写器同时访问
list
,但是由于代码结构的原因,在执行Add()
的方法返回之前,最后一个元素永远不会被访问。 - 未从
list
中删除任何元素。
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先生的观点,因为这一切可以归结为两个问题:
- 列表类是作为平面数组实现的吗?
如果是,则:
- 写指令可以在写过程中被抢占吗>
我相信不是这样的——在任何东西可以读取DWORD或其他东西之前,完整的写入将发生。换句话说,永远不会发生这样的情况:我写入DWORD的四个字节中的两个,然后你读取新值的1/2和旧值的1/2。
所以,如果你通过提供一个指针的偏移量来索引一个数组,你可以在没有线程锁定的情况下安全地读取。如果List做的不仅仅是简单的指针运算,那么它就不是线程安全的。
如果List没有使用平面数组,我想你现在已经看到它崩溃了。
我自己的经验是,在没有线程锁定的情况下,通过索引从List中读取单个元素是安全的。这只是我个人的看法,所以请接受它的价值。
最坏的情况,比如需要遍历列表,最好的做法是:
- 锁定列表
- 创建一个相同大小的数组
- 使用CopyTo()将List复制到数组
- 解锁列表
- 然后遍历数组而不是列表。
在(无论你称之为。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代替。从我的经验来看,基于消息的同步是正确的方法。