【问题标题】:.NET Threading - Synchronization Object.NET 线程 - 同步对象
【发布时间】:2011-06-17 11:42:34
【问题描述】:

我有一个多线程应用程序。

一个线程插入一个队列,许多线程从这个队列中读取。为了正确读取,读取器线程像以下代码一样锁定队列。

我的问题是:当阅读器线程调用以下代码时,inserter线程是否会被阻塞,因为它使用相同的队列?还是继续插入而不中断?

lock ( MsgQueue ) {
    if ( MsgQueue.Count == 0 ) {
         Monitor.Wait( MsgQueue );
         continue;
    }
    msg = MsgQueue.Dequeue( );
}

【问题讨论】:

标签: c# .net multithreading thread-safety


【解决方案1】:

当该线程在lock 中时,另一个线程将被锁(MsgQueue)阻塞但在Monitor.Wait 中时不会(释放锁以便其他线程可以Pulse)。

这是条件变量模式:在处理共享状态(队列实例)时持有锁,但在等待条件改变时释放它(Monitor.Wait)。

更新:基于评论:

不,它只是简单地插入。插入器没有锁

那么队列对象很可能已损坏。除非您使用的队列类型本质上是线程安全的,否则您必须对所有操作使用 same 锁。

更新 #2:如果此队列主要用于将对象从一组(源)线程传输到另一组(工作)线程(其中每组可能只是一个),那么你应该考虑一个线程安全的ConcurrentQueue(尽管你需要像event这样的东西来表示队列中有东西以避免工人轮询)。

【讨论】:

    【解决方案2】:

    是的,当消费者持有锁时,生产者(或插入者)将被阻塞。请注意,锁定是通过调用Monitor.Wait 释放的,然后在控制流返回给调用者时重新获取。所有这些都假设您的生产者尝试获取相同的锁。

    附带说明一下,您对消费者进行编码的方式比它可能的效率要低一些。因为你有一个continue 语句,所以我必须假设一个while 循环包装了lock,这可能会使你的代码看起来更像下面。

    object msg = null;
    while (msg == null)
    {
      lock (MsgQueue) 
      {
        if (MsgQueue.Count == 0) 
        {
          Monitor.Wait(MsgQueue);
          continue;
        }
        msg = MsgQueue.Dequeue();
      }
    }
    

    这可以重构,以便在 lock 块内重新检查等待条件。这样您就不必释放并重新获取锁来执行检查。

    object msg = null;
    lock (MsgQueue) 
    {
      while (MsgQueue.Count == 0) 
      {
        Monitor.Wait(MsgQueue);
      }
      msg = MsgQueue.Dequeue();
    }
    

    再次,因为我看到continue 语句,我假设您知道在Wait 之后必须始终重新检查等待条件。但是,以防万一你不知道这个要求,我会在这里说明,因为它很重要。

    如果没有重新检查等待条件并且有 2 个或更多消费者,那么其中一个可能会进入锁并将最后一个项目出队。即使通过调用PulsePulseAll 将另一个消费者从等待队列移动到就绪队列,这种情况仍然可能发生,但它没有机会在第一个消费者之前重新获得锁。显然,如果没有重新检查,消费者可能会尝试对空队列进行操作。在生产端使用PulsePulseAll 都没有关系。仍然存在问题,因为Monitor 不优先考虑Wait 高于Enter

    更新:

    我忘了指出,如果您使用的是 .NET 4.0,那么您可以利用 BlockingCollection,这是一个阻塞队列的实现。它对多个生产者和消费者是安全的,如果队列为空,它会为您完成所有阻塞。

    【讨论】:

      【解决方案3】:

      插入器线程被阻塞,是的。

          lock ( MsgQueue ) {
              if ( MsgQueue.Count == 0 ) {  // LINE 1
                  Monitor.Wait( MsgQueue ); // LINE 2
                  continue;
              }
              msg = MsgQueue.Dequeue( ); // LINE 3
          }
      

      在第 1 行,读卡器持有锁,因此插入器被阻塞。

      在第 2 行,锁被释放,直到插入器在 MsgQueue 上调用 Monintor.Pulse 时才重新获得。

      在第 3 行,锁仍然被持有(从第 1 行开始),然后由于退出lock 范围而再次释放。

      【讨论】:

        【解决方案4】:

        如果插入器线程调用lock ( MsgQueue ),那么很明显,只要有一个读者锁定了队列,它就会阻塞

        【讨论】:

        • 不,它只是简单地插入。插入器没有锁
        【解决方案5】:

        没有。我认为你的问题是关于lock ( MsgQueue ) 的含义,这个比喻可能有点误导。锁定一个对象不会以任何方式改变该对象的状态,也不会阻塞其他线程,除非这些线程也在同一个对象上使用lock

        这就是为什么你经常看到这种(更好的)模式:

        private Queue<MyClass> _queue = ...;
        private object _queueLock = new object();
        ...
        lock(_queueLock )
        {
            _queue.Enqueue(item);
        }
        

        锁中使用的引用仅用作“票证”。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-01-21
          • 2013-04-02
          • 2016-06-02
          • 1970-01-01
          • 1970-01-01
          • 2011-07-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多