ToArray()可以抛出异常吗?

时间:2013-02-18 21:58:03

标签: c# multithreading collections locking

虽然this question的答案非常好,但它暗示您应该锁定对List.ToArray()的锁定以实现并发。 this blog post也暗示它可能会灾难性地失败(但很少)。我通常在枚举列表或其他集合时使用ToArray而不是锁定,以避免修改集合,枚举可能无法完成"例外。这个答案和博客文章都对这个假设提出质疑。

List.ToArray()的文档没有列出任何异常,所以我一直认为它总是会完成(虽然可能是陈旧的数据),虽然从数据一致性的角度看它不是线程安全的,从代码执行的角度来看,它是线程安全的 - 换句话说,它不会抛出异常并且调用它不会破坏底层集合的内部数据结构。

如果这个假设不正确,那么虽然它从未引起过问题,但它可能是高可用性应用程序中的定时炸弹。什么是明确的答案?

5 个答案:

答案 0 :(得分:6)

由于一个简单的原因,您无法找到有关ToArray方法可能例外的文档。这是一种具有许多“重载”的扩展方法。它们都具有相同的方法签名,但对于不同的集合类型,实现方式不同,例如, List<T>HashSet<T>

但是,我们可以安全地假设.NET框架BCL因性能原因而不执行任何锁定的大多数代码。我还非常具体地检查了ToList的{​​{1}}的实施情况。

List<T>

正如您可能想象的那样,这是非常简单的代码,最终会在public T[] ToArray() { T[] array = new T[this._size]; Array.Copy(this._items, 0, array, 0, this._size); return array; } 中执行。 对于此特定实现,您还可以查看可能发生的异常in MSDN page for Array.Copy method。它归结为一个例外,如果列表的排名在刚刚分配目标数组之后立即改变,则抛出异常。

考虑到mscorlib是一个简单的例子,你可以想象异常的机会在需要更复杂的代码以便存储在数组中的结构上升。 List<T>的实施是一个更有可能失败的候选人:

Queue<T>

答案 1 :(得分:4)

如果文档没有明确保证线程安全,或者原则上你无法承担它。如果你认为它存在风险,那么你就有可能将一类错误投入到生产中,而这些错误可能会被淘汰,并且有可能使你失去大量的生产力/可用性/资金。你愿意冒这个风险吗?

你永远不能测试线程安全的东西。你永远无法确定。您无法确定未来版本的行为方式是否相同。

以正确的方式行事并锁定。

顺便说一句,这些评论是针对List.ToArray的,ToArrayIEnumerable.ToArray更安全的版本之一。我理解为什么人们会错误地认为它可以与写入列表同时使用。当然{{1}} 不可能是threadssafe ,因为这是基础序列的属性。

答案 2 :(得分:2)

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

答案 3 :(得分:0)

首先,您必须明确呼叫站点必须位于线程安全区域。代码中的大多数区域都不是线程安全区域,并且在任何给定时间(对于大多数应用程序代码)将假定执行一个线程。对于(非常粗略的估计)99%的应用程序代码,这个问题没有任何意义。

其次,你必须明确枚举函数的真正含义,因为这将根据你运行的枚举类型而有所不同 - 你是在谈论Enumerations的正常linq扩展吗?

第三,您提供给ToArray代码的链接及其周围的锁定语句充其量是无意义的:没有显示调用点也锁定在同一个集合上,它不能保证线程安全。

等等。

答案 4 :(得分:0)

看来你混淆了两件事:

  • 列表与LT; T&GT;在枚举时不支持被修改。枚举列表时,枚举器会检查每次迭代后列表是否已被修改。在枚举列表之前调用列表&lt; T&gt; .ToArray解决了这个问题,因为您要枚举列表的快照,而不是列表本身。

  • 列表与LT; T&GT;不是一个线程安全的集合。以上所有假设都来自同一个线程。从两个线程访问列表总是需要锁定。 List&lt; T&gt; .ToArray不是线程安全的,在这里没有帮助。