ToArray() 方法会抛出异常吗?

10
尽管回答这个问题很好,但它意味着您应该在并发情况下将对List.ToArray()的调用括在锁中。 这篇博客文章也暗示它可能会失败,尽管很少见。我通常使用ToArray而不是锁定来枚举列表或其他集合,以避免“Collection Modified, Enumeration may not complete”异常。但这个答案和博客文章对这个假设提出了质疑。
List.ToArray()的文档没有列出任何异常,因此我一直认为它总是能完成(尽管可能带有过时数据),虽然从数据一致性的角度来看它不是线程安全的,但从代码执行的角度看,它是线程安全的--换句话说,它不会抛出异常,调用它也不会破坏基础集合的内部数据结构。
如果这个假设不正确,那么虽然它从来没有引起问题,在高可用性应用程序中它可能会成为一个定时炸弹。什么是确定性的答案?

List<T>.Add方法在另一个线程同时修改列表时也不会抛出异常。但它仍然不是线程安全的。它只是检查您是否在同一线程中同时修改和枚举它。如果一个方法没有记录为线程安全,那么你怎么能相信它是线程安全的呢?(假设你谈论的是List<T>.ToArray或Enumerable.ToArray。ConcurrentBag<T>.ToArray是线程安全的,而不使用ToArray枚举ConcurrentBag<T>也是线程安全的。) - dtb
我将问题的范围缩小到了List和List<T>,因为这些是我最关心的。我看到许多流行的开源框架都使用了这种技术,所以我认为我不是唯一一个对这种技术的“安全性”做出假设的人。我不是在问它们是否线程安全(根据定义它们不是),但我应该开始查找和修复任何“不安全”的ToArray()调用吗? - Joe Enzminger
你确定这些开源框架使用的是ToArray而不是锁定,还是它们实际上在同一线程中枚举列表时使用ToArray来修改列表? - dtb
5个回答

7
您不会在ToArray方法可能抛出的异常文档中找到任何信息,因为这是一个扩展方法,它具有许多重载。它们都有相同的方法签名,但对于不同的集合类型,例如List和HashSet,实现是不同的。 然而,我们可以做出一个安全的假设,认为大多数.NET框架BCL代码由于性能原因不执行任何锁定。我还非常特别地检查了List的ToList实现。
public T[] ToArray()
{
    T[] array = new T[this._size];
    Array.Copy(this._items, 0, array, 0, this._size);
    return array;
}

你也许已经想到了,这是一个相当简单的代码,最终会在 mscorlib 中执行。对于此特定实现,您还可以在 MSDN Array.Copy 方法页面 中查看可能发生的异常。问题归结为,如果目标数组分配后列表的级别发生更改,则会抛出异常。
考虑到 List<T> 是一个简单的例子,你可以想象需要更复杂的代码才能将其存储在数组中的结构出现异常的机会会增加。Queue<T> 的实现是更容易失败的候选者:
public T[] ToArray()
{
    T[] array = new T[this._size];
    if (this._size == 0)
    {
        return array;
    }
    if (this._head < this._tail)
    {
        Array.Copy(this._array, this._head, array, 0, this._size);
    }
    else
    {
        Array.Copy(this._array, this._head, array, 0, this._array.Length - this._head);
        Array.Copy(this._array, 0, array, this._array.Length - this._head, this._tail);
    }
    return array;
}

非常棒的分析。这明确地回答了问题。谢谢! - Joe Enzminger
List<T>.ToArray()不是扩展方法... - Yet Another Code Maker

5

当文档或原则没有明确保证线程安全时,您不能假设它是线程安全的。如果您假设它是线程安全的,则会在生产中产生一类无法调试且可能会给您带来大量生产力/可用性/成本损失的错误。您愿意冒这个风险吗?

您永远无法测试某个东西是否线程安全。 您永远不能确定。您无法确定未来版本是否表现相同。

正确的方法是加锁。

顺便说一下,这些备注是针对 List.ToArray 的,它是更安全的 ToArray 版本之一。我理解为什么会错误地认为它可以与对列表的写入同时使用。当然,IEnumerable.ToArray 不可能是线程安全的,因为这是基础序列的属性。


-1是因为答案既过于冗长又过于笼统。有时正确的做法是锁定,但有时正确的做法是选择线程或并发数据结构/算法。此外,您可以对应该是线程安全的代码执行测试以验证它。 - Sam Harwell
@280Z28,我尊重您的批评。答案本意是相当通用的,因为整个问题类别的根本问题都是一样的。但我对您最后的陈述有所怀疑:您会执行哪些测试来确保ToArray是100%安全的?任何不足的地方都会导致生产中的问题。您曾经进行过操作工作吗?我讨厌每天从非确定性错误收到10封错误邮件。 - usr
我相信你已经非常清楚了解这种情况,但是对于那些没有太多经验的开发人员来说,这个特定的表述可能会产生误导作用。你关于测试的陈述不仅适用于测试 ToArray,而且给人的印象是“无法通常实现线程安全的测试”。 - Sam Harwell

3

ToArray 不是线程安全的,这段代码可以证明!

考虑一下这个非常荒谬的代码:

        List<int> l = new List<int>();

        for (int i = 1; i < 100; i++)
        {
            l.Add(i);
            l.Add(i * 2);
            l.Add(i * i);
        }

        Thread th = new Thread(new ThreadStart(() =>
        {
            int t=0;
            while (true)
            {
                //Thread.Sleep(200);

                switch (t)
                {
                    case 0:
                        l.Add(t);
                        t = 1;
                        break;
                    case 1:
                        l.RemoveAt(t);
                        t = 0;
                        break;
                }
            }
        }));

        th.Start();

        try
        {
            while (true)
            {
                Array ai = l.ToArray();

                //foreach (object o in ai)
                //{
                //    String str = o.ToString();
                //}
            }
        }
        catch (System.Exception ex)
        {
            String str = ex.ToString();                 
        }

    }

这段代码很快就会失败,因为l.Add(t)会导致此问题。因为ToArray不是线程安全的,它将把数组分配给当前l的大小,然后我们将在另一个线程中向l添加一个元素,随后它将尝试将l的当前大小复制到ai中,并因为l含有太多元素而失败。 ToArray会抛出一个ArgumentException异常。


1

首先,您需要明确调用方必须位于线程安全区域。对于大多数应用程序代码来说,它们的大多数区域都不是线程安全的,并且会假定在任何给定时间只有一个执行线程(对于大多数应用程序代码)。对于大约99%的所有应用程序代码而言,这个问题并没有实际意义。

其次,您需要明确“枚举”函数到底是什么,因为这将取决于您正在遍历的枚举类型——您是否正在谈论 Enumerations 的普通 linq 扩展?

第三,您提供的 ToArray 代码链接以及周围的 lock 语句根本就是胡说八道:如果不显示调用同一集合上的锁定,它就不能保证线程安全。

等等。


2
我本来想给你点赞的,但是最后一句话真的没有必要。并不是每个人都完全了解线程安全理论,也不能期望他们这样做。 - Simon Whitehead
我被更正了,我的文字也被更正了。感谢你给了我一个好的教训,我点赞了你 ;) - Casper Leon Nielsen

1

看起来你混淆了两件事情:

  • List<T> 不支持在枚举时进行修改。当枚举列表时,枚举器会在每次迭代后检查列表是否已被修改。在枚举列表之前调用 List<T>.ToArray 可以解决这个问题,因为你在枚举列表的快照而不是列表本身。

  • List<T> 不是线程安全的集合。上述所有内容都假定从同一线程访问。从两个线程访问列表始终需要锁定。List<T>.ToArray 不是线程安全的,也无法在此处发挥作用。


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