以下线程是否安全?

时间:2015-06-15 08:08:29

标签: c# multithreading collections concurrency locking

我有以下代码并想知道它是否是线程安全的。我只在我从集合中添加或删除项目时锁定,但在迭代集合时不锁定。迭代时锁定会严重影响性能,因为该集合可能包含数十万个项目。有什么建议可以使这个线程安全吗?

由于

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万个项目的性能开销实际上非常小。

4 个答案:

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

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

  1. 您可以克隆您的列表(我通常会为此目的使用.ToArray()方法)。它将导致内存中的列表加倍,并且所有成本都可能无法获得原位版本的最新结果,或者......
  2. 您可以将整个迭代放在一个锁定的块中,这会导致在执行长时间运行时阻止其他线程访问该阵列。