以下线程安全吗?

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 异常或者 - 更糟糕的是 - 乱码数据。

如果你希望你的代码是线程安全的,你有两个选择:

  1. 您可以克隆您的列表(为此我通常使用 .ToArray() 方法)。这将导致内存中的列表及其所有成本加倍,并且结果可能不是现场版本的最新版本,或者...
  2. 您可以将整个迭代放在一个锁定的块中,这将导致在您执行 long-运行 操作时阻止其他线程访问该数组。