10

虽然这个问题的答案非常好,但这意味着您应该将对 List.ToArray() 的调用包围在一个锁中以实现并发。 这篇博文还暗示它可能会灾难性地失败(但很少)。在枚举列表或其他集合时,我通常使用 ToArray 而不是锁定,以避免“集合已修改,枚举可能无法完成”异常。这个答案和博客文章对这个假设提出了质疑。

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

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

4

5 回答 5

7

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

但是,我们可以对大多数代码做出一个安全的假设,即 .NET 框架 BCL 出于性能原因不执行任何锁定。我还非常具体地检查了ToListfor的实现List<T>

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;
}
于 2013-02-18T22:16:19.050 回答
5

当文档或原则未明确保证线程安全时,您不能假设它。如果您确实假设它,您就有可能将一类无法调试的错误投入生产,并且可能会花费您大量的生产力/可用性/金钱。你愿意承担这个风险吗?

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

以正确的方式进行操作并锁定。

顺便说一句,这些评论是针对List.ToArray哪个版本更安全的ToArray. 我理解为什么人们会错误地认为它可以与写入列表同时使用。当然IEnumerable.ToArray 不可能是线程安全的,因为这是底层序列的属性。

于 2013-02-18T22:12:07.513 回答
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大小的元素。抛出一个.laiToArrayArgumentException

于 2013-02-18T22:26:12.113 回答
1

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

其次,您必须弄清楚枚举函数到底是什么,因为这会因您正在运行的枚举类型而异——您是在谈论枚举的正常 linq 扩展吗?

第三,您提供的指向 ToArray 代码的链接及其周围的 lock 语句充其量是无稽之谈:没有显示调用站点也锁定同一个集合,它根本不能保证线程安全。

等等。

于 2013-02-18T22:08:37.247 回答
1

您似乎混淆了两件事:

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

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

于 2013-02-18T22:18:32.883 回答