【问题标题】:C# Threading and QueuesC# 线程和队列
【发布时间】:2010-10-22 02:37:58
【问题描述】:

这不是关于我可以或应该使用哪些不同的方法来以最佳方式利用队列,而是我所看到的对我来说毫无意义的事情。

void Runner() {
    // member variable
    queue = Queue.Synchronized(new Queue());
    while (true) {
        if (0 < queue.Count) {
            queue.Dequeue();
        }
    }
}

这是在单个线程中运行的:

var t = new Thread(Runner);
t.IsBackground = true;
t.Start();

其他事件在其他地方“排队”。我所看到的情况是在一段时间内,Dequeue 实际上会抛出 InvalidOperationException,队列为空。这应该是不可能的,因为计数如何保证那里有东西,而且我很肯定没有其他东西是“出队”的。

问题:

  1. Enqueue 是否有可能在项目完全进入队列之前增加计数(无论这意味着什么......)?
  2. 线程是否有可能在 Dequeue 语句处以某种方式重新启动(过期、重置...),但在它已经删除一个项目之后立即?

编辑(澄清):

这些代码片段是实现后台帮助线程的 Wrapper 类的一部分。这里的Dequeue是唯一的Dequeue,所有的Enqueue/Dequeue都在Synchronized成员变量(queue)上。

【问题讨论】:

  • 因为 Ryan 的回答......这是真正的代码还是只是一个简化的例子?如果它是真正的代码,您应该真正考虑更改循环 - 轮询队列而不是使读取器与写入器同步是一个糟糕的设计。您正在浪费数百万个处理器周期来加热房间。
  • 这是一个解决问题的例子。那里有一个 Thread.Sleep ,处理器没有被锤击。我选择轮询过程而不是同步读取/写入器的原因是因为队列中几乎一直都有东西。在我们的后备箱中,虽然我添加了一个 AutoResetEvent 来玩。就像我在顶部所说的那样,我并不十分关心这里的实施。这种线程模型似乎存在真正的问题,无论是对还是错。
  • 通过查看您的代码,您至少拥有主线程和您认为调用 Dequeue 的线程。为什么不命名你的线程,每次调用 Dequeue 时,用堆栈跟踪记录线程的名称。您可能会发现主线程中的某些东西的行为方式出乎您的意料。
  • 您应该为此使用BlockingCollection,而不是同步队列。

标签: c# multithreading


【解决方案1】:

另一个让线程安全使用队列的选项是 ConcurrentQueue<T> 类,它自 2009 年(这个问题的年份)开始引入。这可能有助于避免编写自己的同步代码,或者至少有助于使其更简单。

从 .NET Framework 4.6 开始,ConcurrentQueue&lt;T&gt; 还实现了接口 IReadOnlyCollection&lt;T&gt;

【讨论】:

    【解决方案2】:

    “入队”数据的其他区域是否也使用相同的同步队列对象?为了使 Queue.Synchronized 是线程安全的,所有 Enqueue 和 Dequeue 操作必须使用相同的同步队列对象。

    来自MSDN

    为了保证线程的安全 队列,所有操作都必须完成 仅通过此包装器。

    编辑: 如果您正在循环涉及大量计算的许多项目,或者您正在使用长期线程循环(通信等),您应该考虑使用等待函数,例如System.Threading.Thread.SleepSystem.Threading.WaitHandle.WaitOneSystem.Threading.WaitHandle.WaitAll、或 System.Threading.WaitHandle.WaitAny 在循环中,否则可能会影响系统性能。

    【讨论】:

    • "[...] 确保在任何线程循环中都有 System.Threading.Thread.Sleep(1) [...]"!?!什么?为什么要这样做?
    • 啊……明白了。你的意思是while(true) {}。不 - 你真的不应该将 Thread.Sleep() 添加到这个循环中。您不应将 Thread.Sleep() 添加到任何循环中。这只是非常糟糕的设计,应该修复 - 读取线程应该与写入线程同步,而不是轮询队列。
    • 睡眠允许系统共享 CPU 和资源,否则线程会消耗大量资源(在某些情况下会饿死其他线程和进程)。显然这是示例代码,但是有一段时间(true)没有退出条件,所以我提到了这一点。 Sleep(0) 存在不将控制权交给低优先级线程的问题,因此建议使用 Sleep(1)。如果线程循环是定时的或具有有限的时间跨度,则可能不需要休眠。我见过许多 Windows 服务,其中单个 Sleep 语句将 CPU 使用率从接近 80-100% 更改为
    • 我不同意“你不应该将 Thread.Sleep() 添加到任何循环中”。有明确的原因:主要是服务使用无限期运行的线程(直到停止)。正如您所提到的,还有其他可以使用的设计模式,当然还有其他的休眠方式(请参阅 Richter 并发事务文章)。但是有时间和理由何时使用睡眠,但通常只用于具有扫描周期/循环的长时间运行的线程,然后在最外层的线程循环中只使用一次睡眠。
    • 如果 Thread.Sleep(1) 显着降低了处理器的使用率,那么您的循环大部分时间都不执行任何操作。所以你应该使用一些表单同步来让线程只在有工作的情况下运行。即使您将处理器使用率降低到 1%,您仍然在浪费数千个周期——您有一个简单的 if 语句(可能需要 10 个周期进行简单检查)和两个上下文切换(每个周期在 2000 到 8000 个周期之间)。或者从另一边看 - 100 个线程绝对什么都不做会消耗你所有的处理器时间。
    【解决方案3】:

    问题 1:如果您使用的是同步队列,那么:不,您是安全的!但是您需要在供应商和馈线双方使用同步实例。

    问题 2:在没有工作可做时终止工作线程是一项简单的工作。但是,无论哪种方式,您都需要一个监控线程,或者让队列在队列有事情要做时启动一个后台工作线程。最后一个听起来更像是 ActiveObject 模式,而不是一个简单的队列(Single-Responsibily-Pattern 说它应该只做队列)。

    此外,我会选择阻塞队列而不是上面的代码。即使没有工作要做,您的代码的工作方式也需要 CPU 处理能力。阻塞队列让您的工作线程在无事可做时休眠。您可以在不使用 CPU 处理能力的情况下运行多个休眠线程。

    C# 没有提供阻塞队列实现,但有很多实现。请参阅此example 和此one

    【讨论】:

      【解决方案4】:

      使用 Reflector,您可以看到不,直到添加项目后计数才会增加。

      正如 Ben 指出的那样,您似乎确实有多个人调用 dequeue。

      你说你肯定没有其他东西在调用 dequeue。那是因为你只有一个线程调用出队吗? dequeue 是否在其他任何地方调用?

      编辑:

      我写了一些示例代码,但无法重现问题。它只是不停地运行,没有任何异常。

      在出现错误之前它运行了多长时间?也许你可以分享更多的代码。

      class Program
      {
          static Queue q = Queue.Synchronized(new Queue());
          static bool running = true;
      
          static void Main()
          {
              Thread producer1 = new Thread(() =>
                  {
                      while (running)
                      {
                          q.Enqueue(Guid.NewGuid());
                          Thread.Sleep(100);
                      }
                  });
      
              Thread producer2 = new Thread(() =>
              {
                  while (running)
                  {
                      q.Enqueue(Guid.NewGuid());
                      Thread.Sleep(25);
                  }
              });
      
              Thread consumer = new Thread(() =>
                  {
                      while (running)
                      {
                          if (q.Count > 0)
                          {
                              Guid g = (Guid)q.Dequeue();
                              Console.Write(g.ToString() + " ");
                          }
                          else
                          {
                              Console.Write(" . ");
                          }
                          Thread.Sleep(1);
                      }
                  });
              consumer.IsBackground = true;
      
              consumer.Start();
              producer1.Start();
              producer2.Start();
      
              Console.ReadLine();
      
              running = false;
          }
      }
      

      【讨论】:

      • 非常好,我实际上有一个看起来非常接近这个的测试用例,它抓住了要点。我认为该服务在崩溃之前运行了大约 2 周以上。
      • 在这一点上,我只能建议:(1) 你进入了 If 部分,(2) 太阳耀斑将你的位翻转为零,(3) 你的队列抛出了异常。它真的应该被抛出一个 SolarFlareException,但无论如何。波塔托,波塔托。
      【解决方案5】:

      以下是the MSDN page 关于此主题的可能答案:

      通过集合枚举是 本质上不是线程安全的 程序。即使是一个集合 同步,其他线程仍然可以 修改集合,这会导致 枚举器抛出异常。 为了保证线程安全 枚举,您可以锁定 在整个收集 枚举或捕获异常 由其他人所做的更改导致 线程。

      我的猜测是你是对的 - 在某些时候,会发生竞争情况,你最终会出列一些不存在的东西。

      Mutex 或 Monitor.Lock 在这里可能是合适的。

      祝你好运!

      【讨论】:

        【解决方案6】:

        我认为有问题的顺序是:

        1. (0 &lt; queue.Count) 计算结果为真,队列不为空。
        2. 此线程获取preempted 并运行另一个线程。
        3. 另一个线程从队列中删除一个项目,将其清空。
        4. 此线程继续执行,但现在位于 if 块内,并尝试将空列表出列。

        但是,你说没有其他东西正在出队......

        尝试在 if 块中输出计数。如果您看到计数向下跳数,则表明其他人正在出队。

        【讨论】:

        • 这也是我的第一个想法,但他说“我很肯定没有其他东西正在“出列”。”
        • 有点我的想法,但我认为他应该澄清事实,使他如此确定以至于没有其他东西在召唤出队。
        • +1。无论是否有任何其他线程当前出队,Count 和 Dequeue 之间都存在竞争条件。最好解决竞争条件并解决真正的问题。
        猜你喜欢
        • 1970-01-01
        • 2010-10-28
        • 1970-01-01
        • 2011-09-27
        • 2015-02-16
        • 2011-02-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多