当一个线程只负责入队,另一个线程只负责出队时,使用ConcurrentQueue<T>或Queue<T>?

5

我有一个FIFO和两个线程。其中一个线程只会对FIFO进行入队操作,而另一个线程只会对FIFO进行出队操作。我需要使用ConcurrentQueue还是普通的Queue就足够了?


2
你有阅读文档吗?https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/when-to-use-a-thread-safe-collection#concurrentqueuet-vs-queuet 你能说明你的疑惑所在吗? - Jeroen Vannevel
我的困惑在于我认为只要确保每个FIFO的一侧只有一个线程操作,就仍然可以使用Queue<T>。 - Reto
2个回答

10
如果您有多个线程,则需要为Queue对象实例提供线程安全和同步。 为了避免重复发明轮子并自己完成操作,建议使用Microsoft的ConcurrentQueue
MSDN: https://learn.microsoft.com/en-us/dotnet/api/system.collections.queue?view=netframework-4.7.1

如果需要从多个线程同时访问集合,请使用ConcurrentQueue或ConcurrentStack。

如果您使用一个Queue(即非ConcurrentQueue)并且有多个线程更新对象实例,则可能会遇到运行时异常,例如:
- ArgumentOutOfRangeException - ArgumentException ( InvalidOffLen) - ExceptionResource.InvalidOperation_EmptyQueue 如果队列的内部状态正在被修改但尚未完成,由于CPU线程调度,这些异常是可能的。如果另一个线程在它处于不一致状态时访问队列对象实例,则您可能会遇到这些异常。
要查看源代码,请参见:
.Net Framework 4.7.1 https://referencesource.microsoft.com/#System/compmod/system/collections/generic/queue.cs 示例控制台应用程序:
运行以下内容作为您的实验室,您应该会遇到System.InvalidOperationException。 System.InvalidOperationException: 'Collection was modified after the enumerator was instantiated.'
class Program
{
    static Queue<string> Queue = new Queue<string>();

    static void Main(string[] args)
    {
        Thread producer = new Thread(Enqueue);
        Thread consumer = new Thread(Dequeue);

        producer.Start();
        consumer.Start();

        Console.ReadKey();
    }

    static void Enqueue()
    {
        for (int i = 0; i < 10000; i++)
        {
            Queue.Enqueue("Number : " + i);
            SimulateWork();
        }
    }

    static void Dequeue()
    {
        while (true)
        {
            if (Queue.Any())
            {
                Console.WriteLine(Queue.Dequeue());
                SimulateWork();
            }
        }
    }

    static void SimulateWork()
    {
        for (int i = 0; i < 1000000; i++)
        { }
    }
}

这个实验展示了当Queue实例处于不一致状态时会发生什么。即使只有一个生产者和一个消费者,您仍需要适当的同步。如果在EnqueueDequeue操作周围添加锁定或同步,您会发现它可以正常运行。
            lock (lockObject)
            {
                Queue.Enqueue("Number : " + i);
                SimulateWork();
            }


            lock (lockObject)
            {
                if (Queue.Any())
                {
                    Console.WriteLine(Queue.Dequeue());
                    SimulateWork();
                }
            }

说了这么多,我并不建议您手动添加会阻塞的锁。这只是一个实验,帮助您理解为什么要这样做。
微软花费了很多时间提供线程安全的集合,如使用细粒度锁定和无锁机制的ConcurrentQueue

一些并发集合类型使用轻量级同步机制,例如SpinLock、SpinWait、SemaphoreSlim和CountdownEvent,在.NET Framework 4中是新的。这些同步类型通常在将线程置于真正的等待状态之前进行繁忙自旋。当预计等待时间非常短时,自旋远比等待更少的计算开销,后者涉及昂贵的内核转换。对于使用自旋的集合类,这种效率意味着多个线程可以以非常高的速率添加和删除项目。有关自旋与阻止的更多信息,请参见SpinLock和SpinWait。

如上所述,如果您有多个线程,您的Queue对象实例需要线程安全和同步。为了避免重复造轮子并自己编写代码,我强烈建议使用Microsoft的ConcurrentQueue参考资料: 线程安全集合: https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/ 锁关键字: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement 线程: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/threading/index

6

简短回答:是的,即使只有一个写线程和一个读线程,您仍需要一个线程安全的解决方案。

使用ConcurrentQueue会更容易。如果您愿意,可以使用Queue,但必须自己进行锁定。


2
什么是详细的答案? - Enigmativity

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