【问题标题】:condition_variable, references and threads: Who Owns The Lock?条件变量、引用和线程:谁拥有锁?
【发布时间】:2020-05-26 02:26:58
【问题描述】:

假设我有一个持有std::queue 的类ThreadQueue,并且我将每个std::ref 的一个实例传递给一个线程。进一步假设,线程 1(主线程)创建并持有 ThreadQueue 对象并将消息倒入其中,第二个线程的任务是在收到这些消息时将它们放在某个地方,例如,将它们写入日志文件。

类看起来像:

#include <queue>
#include <mutex>
#include <condition_variable>

using namespace std;

template <typename T>
class ThreadQueue
{
    queue<T> q_;
    mutex mtx;
    unique_lock<mutex> lck;
    condition_variable cv;

public:
    ThreadQueue() { lck = unique_lock<mutex>(mtx); }
    ~ThreadQueue() { if (lck.owns_lock()) lck.unlock(); }

    void enqueue (const T&);
    T dequeue ();
};

template <typename T>
void ThreadQueue<T>::enqueue (const T& t)
{
    lck.lock();
    q_.push(t);
    lck.unlock();
    cv.notify_one();
}

template <typename T>
T ThreadQueue<T>::dequeue ()
{
    cv.wait(lck);
    lck.lock();
    T t = q_.front(); // let's assume that's a copy assignment, because
    q_.pop();         // pop() calls the descructor.
    lck.unlock();
    return t;
}

然后主要的曲调是:

ThreadQueue<std::pair<int, std::string>> logs;
// and maybe something like:
std::thread logger(std::ref(logs));

关键行是cv.wait(lck); 文档明确指出lck 必须是一个unique_lock 对象,其互斥对象当前已被该线程锁定。

现在的问题是:谁真正锁定了互斥锁,谁拥有锁,线程 1 还是线程 2?

【问题讨论】:

  • 你已经为任何使用这个类的人创建了一个解除锁。关键是创建一个std::unique_lock 作为线程安全函数的局部变量,并将其作为对`std::consition_variable 的引用传递。没有必要保留一个唯一的锁作为成员变量。

标签: c++ c++11 concurrency condition-variable


【解决方案1】:

代码中有两个主要错误:

  1. unique_lock 不应该是成员变量。它必须在堆栈上创建,以便在离开作用域时(正常返回或异常时)自动为您释放锁。
  2. cv.wait 必须在您检查队列确实为空后才能调用。 std::condition_variable 是一种无状态通信机制,如果在没有服务员的情况下发出信号,则信号丢失。也有虚假的唤醒。您可能希望使用 cv.wait([this] { return !q_.empty(); }); 为您正确处理条件变量的等待。

例如:

using namespace std;

template <typename T>
class ThreadQueue
{
    queue<T> q_;
    mutex mtx;
    condition_variable cv;

public:
    void enqueue (const T&);
    T dequeue ();
};

template <typename T>
void ThreadQueue<T>::enqueue (const T& t)
{
    {
        lock_guard<mutex> lck(mtx);
        q_.push(t);
    }
    cv.notify_one(); // Optimization: release the lock before signalling.
}

template <typename T>
T ThreadQueue<T>::dequeue ()
{
    unique_lock<mutex> lck(mtx);
    cv.wait(lck, [this] { return !q_.empty(); });
    T t = q_.front();
    q_.pop();
    return t;
}

谁拥有锁?

已锁定互斥锁的线程拥有锁,或进入临界区std::lock_guardstd::unique_lock 都在这里将互斥锁锁定在构造函数中并在析构函数中解锁(在正常范围退出或异常时)。

【讨论】:

  • 好的,非常感谢@Maxim 的这次编辑。我仍然对 C++ 感到满意,而且您确实使我免于数小时可能最不愉快的调试。但是,问题仍然存在:如果线程 1 将队列 每个引用 传递给线程 2,并且线程 2 调用 dequeue(),这样就锁定了互斥锁,谁拥有锁?
  • @SlabSlob unique_locklock_guard 根本不应该共享。应该共享的是mutex,它才是拥有的。当lock_guard&lt;mutex&gt; lck(mtx); 返回时,lck 拥有mtx(已锁定)。
  • @SlabSlob, Re, “...谁拥有锁?”某些互斥体m 的所有者始终是调用m.lock() 的线程。当你声明一个局部变量unique_lock&lt;mutex&gt; ul(m),那么构造函数会调用m.lock(),而析构函数会调用m.unlock(),所以进入该变量作用域的任何线程都必须是互斥体m的所有者。因为它在ul的范围内执行。
  • 感谢@all 澄清这一点。我相信我开始明白了。 (也许 Perl 仍然在我的脑海中混乱......)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-10
  • 1970-01-01
  • 2015-06-19
  • 1970-01-01
  • 2018-03-05
  • 1970-01-01
  • 2021-10-16
相关资源
最近更新 更多