线程安全的重入队列使用peek

时间:2013-03-05 22:10:14

标签: c# thread-safety queue peek

我的基本问题是,如果队列为空,则需要立即处理队列中的项目,或者将项目添加到队列中,如果有项目已经处理则离开。

我正在尝试一种使用peek来简化事物的技术,并且想知道可能存在的问题可能会阻碍。谢谢!

    void SequenceAction(Action action) {
       bool go = false;

       lock (_RaiseEventQueueLock) {
          _RaiseEventQueue.Enqueue(action);
          go = (_RaiseEventQueue.Count == 1); 
       }

       // 'go' can only be true if queue was empty when queue 
       //  was locked and item was enqueued.
       while (go) {
          #if naive_threadsafe_presumption 
          // Peek is threadsafe because in effect this loop owns
          //  the zeroeth item in the queue for as long as the queue 
          //  remains non-empty.
          (_RaiseEventQueue.Peek())();

          #else
          Action a;
          lock (_RaiseEventQueueLock) {
             a = _RaiseEventQueue.Peek();
          }
          a();
          #endif   

          // Lock the queue to see if any item was enqueued while
          //  the zeroeth item was being processed.
          // Note that the code processing an item can call this 
          //  function reentrantly, adding to its own todo list
          //  while insuring that that each item is processed 
          //  to completion.
          lock (_RaiseEventQueueLock) {
             _RaiseEventQueue.Dequeue();
             go = (_RaiseEventQueue.Count > 0); 
          }
       }
    }

2 个答案:

答案 0 :(得分:1)

实际上,您的Peek不是线程安全的。将项添加到队列可能会导致后端存储(最终是一个数组)的大小调整。我想这个队列是在一个循环缓冲区中实现的,带有用于插入和移除的头部和尾部索引。

想象一下,如果队列中有16个项目会发生什么。 Insert位于8,Remove位于9.队列已满。然后发生这种情况:

  1. 线程A调用Peek,检索Remove index(9)。
  2. 线程A被换出。
  3. 线程B调用Enqueue并发现它必须增长队列。
  4. 线程B分配一个包含32个项目的新数组,并从现有数组中复制数据。数据按顺序复制,从“移除”和“环绕”开始。
  5. 线程B将删除设置为0并插入到16。
  6. 线程A获取下一个时间片并返回位置9的项目。
  7. 您刚刚处理了一个无序的事件,您最终会再次处理它。
  8. 更糟糕的是,您将删除位置0处的项目而不进行处理。
  9. 可能能够通过以下方式解决该问题:

    Action nextAction;
    lock (_RaiseEventQueueLock)
    {
        nextAction = _RaiseEventQueue.Peek();
    }
    nextAction();
    
    但是,我不会把我的职业生涯放在上面。我建议使用BlockingCollection和生产者/消费者设计。

    可能的解决方法

    我觉得以下情况应该符合你的意图。

    private readonly object _queueLock = new object();
    private readonly object _processLock = new object();
    
    void SequenceAction(Action action)
    {
        lock (_queueLock)
        {
            _RaiseEventQueue.Enqueue(action);
        }
        if (Monitor.TryEnter(_processLock))
        {
            while (true)
            {
                Action a;
                lock (_queueLock)
                {
                    if (_RaiseEventQueue.Count == 0) return;
                    a = _RaiseEventQueue.Dequeue();
                }
                a();
            }
            Monitor.Exit(_processLock);
        }
    }
    

答案 1 :(得分:1)

    // If action already in progress, add new
    //  action to queue and return.
    // If no action in progress, begin processing
    //  the new action and continue processing
    //  actions added to the queue in the meantime.
    void SequenceAction(Action action) {
       lock (_SequenceActionQueueLock) {
          _SequenceActionQueue.Enqueue(action);
          if (_SequenceActionQueue.Count > 1) {
             return;
          }
       }
       // Would have returned if queue was not empty
       //  when queue was locked and item was enqueued.
       for (;;) {
          action();
          lock (_SequenceActionQueueLock) {
             _SequenceActionQueue.Dequeue();
             if (_SequenceActionQueue.Count == 0) {
                return;
             }
             action = _SequenceActionQueue.Peek();
          }
       }
    }