ConcurrentQueue ToList()与ToArray().ToList()的区别。

4

我不是百分之百确定,所以我想听取专家的意见。

ConcurrentQueue<object> queue = new ConcurrentQueue<object>();

List<object> listA = queue.ToArray().ToList();   // A
List<object> listB = queue.ToList();             // B

我知道ToArray()方法会复制一个数组(因为它是在ConcurrentQueue内部的方法),但是直接调用ToList()方法会不会做同样的事情呢?

简单地说,从A重构代码到B是安全的吗?


将鼠标光标悬停在 .ToList() 上,它是作为队列的实例方法显示,还是作为 Enumerable 的扩展方法显示? - Lasse V. Karlsen
除非您要添加或删除结果列表中的项目(或传递给需要列表的方法或属性),否则您可以始终只使用数组。 - juharr
请记住,接受的答案取决于ToList的内部实现,通常不是线程安全的。例如,如果您使用ConcurrentDictionary执行相同操作,则不安全且可能会引发异常。因此,我的意思是最好不要养成这种习惯,因为ConcurrentQueue是一种幸运的情况。 - Evk
1个回答

6
如果我们查看源代码,我们会发现GetEnumerator也是线程安全的,因此我认为A和B都是线程安全的。
当您调用.ToList()时,Linq会调用List的构造函数。
 public List(IEnumerable<T> collection) {

所以这段代码实际上是创建了一个类似于线程安全的副本:

        using(IEnumerator<T> en = collection.GetEnumerator()) {
            while(en.MoveNext()) {
                Add(en.Current);                                    
            }

ConcurrentQueue的源代码

List的源代码

public IEnumerator<T> GetEnumerator()
{
    // Increments the number of active snapshot takers. This increment must happen before the snapshot is 
    // taken. At the same time, Decrement must happen after the enumeration is over. Only in this way, can it
    // eliminate race condition when Segment.TryRemove() checks whether m_numSnapshotTakers == 0. 
    Interlocked.Increment(ref m_numSnapshotTakers);

    // Takes a snapshot of the queue. 
    // A design flaw here: if a Thread.Abort() happens, we cannot decrement m_numSnapshotTakers. But we cannot 
    // wrap the following with a try/finally block, otherwise the decrement will happen before the yield return 
    // statements in the GetEnumerator (head, tail, headLow, tailHigh) method.           
    Segment head, tail;
    int headLow, tailHigh;
    GetHeadTailPositions(out head, out tail, out headLow, out tailHigh);

    //If we put yield-return here, the iterator will be lazily evaluated. As a result a snapshot of
    // the queue is not taken when GetEnumerator is initialized but when MoveNext() is first called.
    // This is inconsistent with existing generic collections. In order to prevent it, we capture the 
    // value of m_head in a buffer and call out to a helper method.
    //The old way of doing this was to return the ToList().GetEnumerator(), but ToList() was an 
    // unnecessary perfomance hit.
    return GetEnumerator(head, tail, headLow, tailHigh);
}

调查员的备注还提到了我们可以同时使用它:

    /// The enumeration represents a moment-in-time snapshot of the contents
    /// of the queue.  It does not reflect any updates to the collection after 
    /// <see cref="GetEnumerator"/> was called.  The enumerator is safe to use
    /// concurrently with reads from and writes to the queue.

只是为了明确,您是说重构从A到B是可以的,因为它们都能完成相同的任务,同时保持线程安全? - Svek
@Svek,是的,它们是线程安全的,你可以放心地用一个替换另一个。 - gabba

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