【问题标题】:ConcurrentQueue<T> or Queue<T> when one thread only ever enqueues and another thread only ever dequeuesConcurrentQueue<T> 或 Queue<T> 当一个线程只入队而另一个线程只出队时
【发布时间】:2018-04-29 11:28:49
【问题描述】:

我有一个 FIFO 和两个线程。一个线程只会排入 FIFO,而另一个线程只会从 FIFO 出列。我需要拿 ConcurrentQueue 还是 Queue 够用?

【问题讨论】:

标签: c# multithreading queue fifo


【解决方案1】:

如果您有多个线程,则您的Queue 对象实例需要线程安全和同步。为了避免重新发明轮子并自己动手,我建议使用微软的ConcurrentQueue

MSDN: https://docs.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 线程调度尚未完成,则可能出现异常。如果另一个线程访问处于不一致状态的 Queue 对象实例,您可能并且很可能会遇到这些异常。

要审查的源代码:

.Net 框架 4.7.1 https://referencesource.microsoft.com/#System/compmod/system/collections/generic/queue.cs

示例控制台应用程序:

作为您的实验室运行以下命令,您应该会遇到 System.InvalidOperationException

System.InvalidOperationException: '集合在枚举器被实例化后被修改。'

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();
                }
            }

话虽如此,我建议您手动添加会阻塞的锁。这更像是一个实验室练习,可帮助您了解为什么

Microsoft 已投入大量时间为我们提供线程安全集合,例如使用细粒度锁定和无锁机制的 ConcurrentQueue

一些并发集合类型使用轻量级 SpinLock、SpinWait、SemaphoreSlim 等同步机制, 和 CountdownEvent,它们是 .NET Framework 4 中的新功能。这些 同步类型通常使用短暂的忙旋转 在他们将线程置于真正的等待状态之前。当等待时间 预计很短,旋转的计算量要少得多 比等待更昂贵,这涉及昂贵的内核转换。 对于使用旋转的集合类,这种效率意味着 多个线程可以以非常高的速率添加和删除项目。为了 有关旋转与阻塞的更多信息,请参阅 SpinLock 和 旋转等待。

如上所述,如果您有多个线程,则您的Queue 对象实例需要线程安全和同步。为了避免重新发明轮子并自己动手,我强烈建议使用 Microsoft 的 ConcurrentQueue

参考资料:

线程安全集合:

https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/

锁定关键字

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement

线程:

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/threading/index

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 长答案是什么?
    猜你喜欢
    • 2015-11-02
    • 2018-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多