1

为什么作者认为以下部分源代码会导致比赛?

作者说:

如果有多个线程从队列中删除项目,这种设计会受到对 empty、front 和 pop 调用之间的竞争条件的影响,但在单消费者系统中(如这里所讨论的),这不是问题。

这是代码:

template<typename Data>
class concurrent_queue
{
private:
    std::queue<Data> the_queue;
    mutable boost::mutex the_mutex;
public:
    void push(const Data& data)
    {
        boost::mutex::scoped_lock lock(the_mutex);
        the_queue.push(data);
    }

    bool empty() const
    {
        boost::mutex::scoped_lock lock(the_mutex);
        return the_queue.empty();
    }

    Data& front()
    {
        boost::mutex::scoped_lock lock(the_mutex);
        return the_queue.front();
    }

    Data const& front() const
    {
        boost::mutex::scoped_lock lock(the_mutex);
        return the_queue.front();
    }

    void pop()
    {
        boost::mutex::scoped_lock lock(the_mutex);
        the_queue.pop();
    }
};
4

4 回答 4

10

如果您打电话empty,请检查弹出元素是否安全。在线程系统中可能发生的情况是,在您检查队列不为空之后,另一个线程可能已经弹出了最后一个元素,并且队列不为空不再安全。

thread A:                                 thread B:
if(!queue.empty());                            
                                          if(!queue.empty());

                                          queue.pop();

->it is no longer sure that the queue 
  isn't empty
于 2012-04-13T21:09:06.673 回答
4

如果您有多个线程从队列中“获取”数据,则可能会以一种特别糟糕的方式导致竞争条件。取以下伪代码:

class consumer
{
  void do_work()
  {
      if(!work_.empty())
      {
         type& t = work_.front();
         work_.pop();

         // do some work with t
         t...
      }
  }

  concurrent_queue<type> work_;
};

这看起来很简单,但是如果您有多个consumer对象,并且concurrent_queue. 如果消费者在调用之后empty()但在调用之前被打断pop(),那么可能consumer会有多个 s 尝试处理同一个对象。

更合适的实现将在接口中公开的单个操作中执行空检查和弹出,如下所示:

class concurrent_queue
{
private:
    std::queue<Data> the_queue;
    mutable boost::mutex the_mutex;
public:
    void push(const Data& data)
    {
        boost::mutex::scoped_lock lock(the_mutex);
        the_queue.push(data);
    }

    bool pop(Data& popped)
    {
        boost::mutex::scoped_lock lock(the_mutex);
        if(!the_queue.empty())
        {
            popped = the_queue.front();
            the_queue.pop();
            return true;
        }

        return false;
    }
};
于 2012-04-13T21:10:26.143 回答
3

因为你可以这样做...

if (!your_concurrent_queue.empty())
    your_concurrent_queue.pop();

pop...并且如果另一个线程pop在这两行之间调用,仍然会失败。

(这在实践中是否真的会发生,取决于并发线程的执行时间——本质上是线程“竞赛”,谁赢得了这场竞赛,决定了这个错误是否会出现,这在现代抢占式操作系统上基本上是随机的。这种随机性可以使竞争条件非常难以诊断和修复。)

每当客户端执行像这样的“元操作”时(其中有几个调用的序列来实现所需的效果),仅通过方法内锁定来防止竞争条件是不可能的。

而且由于客户端无论如何都必须执行自己的锁定,因此出于性能原因,您甚至可以考虑放弃方法内锁定。只需确保清楚地记录这一点,以便客户知道您没有对线程安全做出任何承诺。

于 2012-04-13T21:29:05.920 回答
2

我认为您感到困惑的是,在您发布的代码中,没有任何东西会导致竞争条件。竞争条件将由实际调用此代码的线程引起。想象一下线程 1 检查线程是否为空。然后该线程休眠一年。一年后当它醒来时,该线程假设队列仍然是空的仍然有效吗?好吧,不,与此同时,另一个线程很容易出现并称为推送。

于 2012-04-13T21:16:19.533 回答