在查看与此问题无关的一些数据库代码时,我注意到在某些地方List<T>
使用不当。具体做法是:
List
作为读者,但使用索引进入list
而不是enumerators
。list
有一位作家。list
,但由于代码结构, last 元素永远不会被访问直到执行Add()
的方法返回。list
。通过 C#文档,这不应该是线程安全的。
但它从未失败过。我想知道,因为List
的具体实现(我假设内部是一个数组,当空间用完时重新分配),它是1-writer 0-enumerator n-reader add-only scenario 意外线程安全,或者是否有一些不太可能的情况,这可能会在当前的 .NET4 实现中爆炸?
编辑:重要细节我遗漏了阅读一些回复。读者将List
及其内容视为只读。
答案 0 :(得分:2)
这可以而且将会受到打击。它还没有。过时的指数通常是第一件事。只要你不想要它就会爆炸。你现在可能很幸运。
当你使用.Net 4.0时,我建议将列表从System.Collections.Concurrent更改为合适的集合,这保证是线程安全的。如果你需要查找一些内容,我也会避免使用数组索引并切换到ConcurrentDictionary:
答案 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,因为这一切归结为两个问题:
如果是,那么:
我相信情况并非如此 - 完全写入将在任何事物可以读取DWORD或其他之前发生。换句话说,我永远不会发生写入DWORD的四个字节中的两个,然后读取新值的1/2和旧值的1/2。
因此,如果您通过为某个指针提供偏移量来索引数组,则可以安全地读取而不进行线程锁定。如果List不仅仅是简单的指针数学,那么它就不是线程安全的。
如果List没有使用平面阵列,我想你现在已经看到它崩溃了。
我自己的经验是,通过索引从List中读取单个项目而不进行线程锁定是安全的。这只是恕我直言,所以把它当作它的价值。
最糟糕的情况是,如果您需要遍历列表,最好的办法是:
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。我可以从经验中说出,基于消息的同步是正确的方法。