List<T>.Contains()是一个线程安全的调用吗 - C#

12

我理解的是,如果在C#中使用通用列表(List),它可以支持多个并发读取器但只有一个写入器。而当引入一个写入器时,必须提供同步构造以使操作线程安全。

List.Contains被认为是读取操作吗?换句话说,如果我调用这个方法,是否需要担心写入器可能同时写入这个List?


考虑函数的工作原理。它很可能使用for循环(如果是排序列表,则使用其他内容),如果同时进行List<T>.Remove()调用,则可以轻松检查数组边界外的值,导致抛出“数组索引超出范围”的异常。 - Spencer Ruport
10个回答

23

是的,您应该这样做。基本上,如果列表可能同时用于写入,则我会为任何操作进行同步。

通常,我发现集合分为两类 - 一种是创建、初始化后再也不会改变的(线程安全),另一种是随着时间的推移而发生变化的(不是线程安全的,在所有访问时都需要锁定)。


5
如果您实例化一个集合、填充它,然后永远不再使用它,我认为将其包装在ReadOnlyCollection<T>()中是有帮助的。这并不能阻止使用者更改集合中的底层项目,但如果您丢弃了对原始列表的引用,则迭代是安全的。 - Scott Whitlock
Scott:同意 - 这使得问题更加清晰。个人而言,我喜欢使用集合类型,在构建后立即保证不可变性(如有必要,使用构建器类型),但这些类型目前尚未在核心框架中提供 :( - Jon Skeet

5
如果您使用Reflector来检查代码,您会得到类似于以下内容的结果:
public bool Contains(T item)
    {
        if (item == null)
        {
            for (int j = 0; j < this._size; j++)
            {
                if (this._items[j] == null)
                {
                    return true;
                }
            }
            return false;
        }
        EqualityComparer<T> comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < this._size; i++)
        {
            if (comparer.Equals(this._items[i], item))
            {
                return true;
            }
        }
        return false;
    }

正如您所看到的,这是一个简单的遍历操作,绝对是一个“读取”操作。如果您仅用于读取(并且没有任何变异项),则无需锁定。如果您在单独的线程中开始修改列表,则必须同步访问。


那看起来不对。即使它是一个读取操作,它仍然在 _size 变量和内部数组的索引上旋转。 这些正是可能抛出越界异常的地方。 @JonSkeet ? - eran otzap

2
< p > List<T>.Contains 绝对是一个读取操作。当您读取它时,可能会有其他线程正在向集合中写入数据。


如果在运行Contains时集合发生了变化呢? - Matthew Olenik

2

是的,你必须担心! List.Contains方法只是获取一个EqualityComparer并循环遍历列表中包含的所有项,将传递的参数与当前迭代索引处的项进行比较,因此如果在迭代时修改了列表,则结果可能是不可预测的。


1
在多线程环境中,您需要确保没有同时写入集合。以下是来自反编译器的代码,集合本身没有为您提供任何锁定功能,因此需要您自己实现。
public bool Contains(T item)
{
    if (item == null)
    {
        for (int j = 0; j < this._size; j++)
        {
            if (this._items[j] == null)
            {
                return true;
            }
        }
        return false;
    }
    EqualityComparer<T> comparer = EqualityComparer<T>.Default;
    for (int i = 0; i < this._size; i++)
    {
        if (comparer.Equals(this._items[i], item))
        {
            return true;
        }
    }
    return false;
}

1

根据文档所述...

枚举集合本质上不是线程安全的过程。

因此,我认为它不是线程安全的。你应该对其进行锁定。


1

可以安全地假设这不是一个线程安全的操作。MSDN描述总结如下:

...此方法使用集合对象的Equals和CompareTo方法来确定项是否存在。

因此,在读取操作之后是一个比较操作。


0

如果一个写入者可能同时进行写入操作,List.Contains绝对不是线程安全的。您需要用锁包装它以及任何其他的读取和写入操作。


0

这被视为读操作。您不会遇到任何竞争条件,但如果您关心获取最新信息,可以将List设为volatile


0
根据MSDN文档

此类型的公共静态成员(在Visual Basic中为Shared)是线程安全的。任何实例成员都不能保证是线程安全的。

ReaderWriterLock类似乎是为您寻找的同步而构建的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接