为什么作者声称这段代码会导致种族

Why does the author claim that this code leads to race?

本文关键字:代码 段代码 为什么      更新时间:2023-10-16

为什么作者认为下面的部分源代码会导致种族?

作者说:

如果有多个线程从队列中删除项目,则此设计受调用 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();
    }
};

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

thread A:                                 thread B:
if(!queue.empty());                            
                                          if(!queue.empty());
                                          queue.pop();
->it is no longer sure that the queue 
  isn't empty
如果有多个

线程"消耗"队列中的数据,则可能会导致争用条件特别糟糕。 采用以下伪代码:

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将尝试处理同一个对象。

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

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

因为你可以这样做...

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

。并且如果另一个线程调用pop"介于"这两行之间,pop仍然失败。

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

每当客户端执行这样的"元操作"(其中有一系列多个调用实现预期效果)时,仅通过方法内锁定就不可能防止争用条件。

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

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