以下线程安全吗?
Is the following thread safe?
我有以下代码,想知道它是否是线程安全的。我只在我从集合中添加或删除项目时锁定,但在遍历集合时不锁定。迭代时锁定会严重影响性能,因为集合可能包含数十万个项目。有什么建议可以使该线程安全吗?
谢谢
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 万个项目的迭代的性能开销实际上非常小。
不,它不是线程安全的,因为当您查看集合内部时可能会对其进行修改...您可以做什么:
Item[] items;
lock (myListLock)
{
items = _items.ToArray();
}
foreach (var item in items)
{
var dp = item.DataPoint;
}
所以你在循环之前在 lock
中复制集合。这显然会使用内存(因为你必须复制 List<>
)(ConcurrentBag<>.GetEnumerator()
几乎完全如此)
请注意,这仅在 Item
是线程安全的(例如,因为它是不可变的)时才有效
不,不是。请注意,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()。
理论上你的代码不是线程安全的。
在后台 foreach
执行正常的 for 循环,如果您在 foreach
遍历列表时从不同的线程添加项目,则可能会遗漏一个项目。此外,如果您 删除 一个项目(从不同的线程),您可能会遇到 AV 异常或者 - 更糟糕的是 - 乱码数据。
如果你希望你的代码是线程安全的,你有两个选择:
- 您可以克隆您的列表(为此我通常使用
.ToArray()
方法)。这将导致内存中的列表及其所有成本加倍,并且结果可能不是现场版本的最新版本,或者...
- 您可以将整个迭代放在一个锁定的块中,这将导致在您执行 long-运行 操作时阻止其他线程访问该数组。
我有以下代码,想知道它是否是线程安全的。我只在我从集合中添加或删除项目时锁定,但在遍历集合时不锁定。迭代时锁定会严重影响性能,因为集合可能包含数十万个项目。有什么建议可以使该线程安全吗?
谢谢
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 万个项目的迭代的性能开销实际上非常小。
不,它不是线程安全的,因为当您查看集合内部时可能会对其进行修改...您可以做什么:
Item[] items;
lock (myListLock)
{
items = _items.ToArray();
}
foreach (var item in items)
{
var dp = item.DataPoint;
}
所以你在循环之前在 lock
中复制集合。这显然会使用内存(因为你必须复制 List<>
)(ConcurrentBag<>.GetEnumerator()
几乎完全如此)
请注意,这仅在 Item
是线程安全的(例如,因为它是不可变的)时才有效
不,不是。请注意,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()。
理论上你的代码不是线程安全的。
在后台 foreach
执行正常的 for 循环,如果您在 foreach
遍历列表时从不同的线程添加项目,则可能会遗漏一个项目。此外,如果您 删除 一个项目(从不同的线程),您可能会遇到 AV 异常或者 - 更糟糕的是 - 乱码数据。
如果你希望你的代码是线程安全的,你有两个选择:
- 您可以克隆您的列表(为此我通常使用
.ToArray()
方法)。这将导致内存中的列表及其所有成本加倍,并且结果可能不是现场版本的最新版本,或者... - 您可以将整个迭代放在一个锁定的块中,这将导致在您执行 long-运行 操作时阻止其他线程访问该数组。