我有以下代码并想知道它是否是线程安全的。我只在我从集合中添加或删除项目时锁定,但在迭代集合时不锁定。迭代时锁定会严重影响性能,因为该集合可能包含数十万个项目。有什么建议可以使这个线程安全吗?
由于
public class Item
{
public string DataPoint { get; private set; }
public Item(string dataPoint)
{
DataPoint = dataPoint;
}
}
public class Test
{
private List<Item> _items;
private readonly object myListLock = new object();
public Test()
{
_items = new List<Item>();
}
public void Subscribe(Item item)
{
lock (myListLock)
{
if (!_items.Contains(item))
{
_items.Add(item);
}
}
}
public void Unsubscribe(Item item)
{
lock (myListLock)
{
if (_items.Contains(item))
{
_items.Remove(item);
}
}
}
public void Iterate()
{
foreach (var item in _items)
{
var dp = item.DataPoint;
}
}
}
修改
我很好奇并且再次在未锁定的迭代与在myListLock
内的锁内迭代之间进行性能分析,并且锁定迭代超过1000万个项目的性能开销实际上非常小。
答案 0 :(得分:2)
不,它不是线程安全的,因为当你查看它时可以修改集合......你可以做什么:
Item[] items;
lock (myListLock)
{
items = _items.ToArray();
}
foreach (var item in items)
{
var dp = item.DataPoint;
}
所以你在lock
内复制集合,然后再骑它。这显然会使用内存(因为你必须复制List<>
)(ConcurrentBag<>.GetEnumerator()
几乎就是这样)
请注意,仅当Item
是线程安全的时(例如因为它是不可变的)才有效
答案 1 :(得分:0)
不,不是。请注意,MSDN上记录的所有类都有一个关于线程安全的部分(接近结尾):https://msdn.microsoft.com/en-us/library/6sh2ey19%28v=vs.110%29.aspx
GetEnumerator的文档还有一些注释:https://msdn.microsoft.com/en-us/library/b0yss765%28v=vs.110%29.aspx
关键是迭代本身不是线程安全的。即使从集合中读取的每个单独的迭代都是线程安全的,但是如果修改了集合,则一致的迭代通常会中断。您可能会遇到诸如两次读取相同元素或跳过某些元素之类的问题,即使集合本身永远不会处于不一致状态。
顺便说一下,你的Unsubscribe()正在对列表进行两次线性搜索,这可能不是你想要的。你不应该在Remove()之前调用Contains()。
答案 2 :(得分:0)
您可以考虑使用真正线程安全的Concurrent Dictionary,这样可以避免让您头痛......
答案 3 :(得分:0)
理论上,您的代码不是线程安全的。
在后台foreach
执行普通的for循环,如果在foreach
遍历列表时从不同的线程添加项目,则可能会遗漏某个项目。此外,如果你删除一个项目(来自一个不同的线程),你可能会得到一个AV异常,或者 - 更糟糕的是 - 乱码数据。
如果您希望代码是线程安全的,您有两种选择:
.ToArray()
方法)。它将导致内存中的列表加倍,并且所有成本都可能无法获得原位版本的最新结果,或者......