【问题标题】:How to assert if a std::mutex is locked?如何断言 std::mutex 是否被锁定?
【发布时间】:2014-02-19 21:33:23
【问题描述】:

使用 GCC 4.8.2(在 Linux/Debian/Sid 64 位上)-或 GCC 4.9(如果可用)-在 C++11 中-我有一些互斥体

std::mutex gmtx;

实际上,它是某个类Foo 中的static 成员,其中包含alphabeta 下面的方法。

它被锁定在alphalike

void alpha(void) {
   std::lock_guard<std::mutex> g(gmtx);
   beta(void);
   // some other work
}

我想检查beta 确实gmtx 已锁定:

void beta(void) {
   assert (gmtx.is_locked());
   // some real work
}

(注意is_locked 只在assert 内部调用...可能效率非常低,甚至有时不准确)

当然,我还有其他函数调用beta,例如

void gamma(void) {
   std::lock_guard<std::mutex> g(gmtx);
   beta();
   // some other work
}

但是is_locked 不存在....我应该如何定义它? (实际上我想确保互斥锁已被某些 [indirect] 调用者锁定在同一个线程中......)

(我想用assert 进行测试的原因是beta 可以在其他地方调用)

我不能使用try_lock(除非使用递归互斥锁),因为在常见情况下它会锁定一个已经锁定的互斥锁...(被调用者锁定在同一个线程中)这不仅是未定义的行为,而且会阻塞完全。

我想避免递归互斥锁(比普通互斥锁更昂贵),除非我真的必须这样做。


注意:真正的程序有点复杂。实际上,所有方法都在一个类中,该类在“项目”上保持命名双向关系。所以我在那个类里面有一个从项目到名称的映射和另一个从名称到项目的映射。 beta 将是添加真正命名的内部方法,alphagamma 将是通过项目名称或项目名称查找或添加项目的方法。

PS:真正的程序还没有发布,但应该成为MELT的一部分——它的未来monitor;您可以从here(临时位置)下载它(alpha 阶段,非常错误)

【问题讨论】:

  • 如果beta 要求互斥锁始终处于锁定状态(正如您的assert 所建议的那样),您为什么要将该责任推到调用链上?
  • 因为(正如我编辑的那样)像gamma 这样的其他函数会调用beta ...
  • 是的,我明白了,但是两者都需要在调用 beta 之前锁定互斥锁,那么为什么 beta 不只是锁定互斥锁呢?你是说两个调用者都有其他需要锁定互斥锁的工作吗?
  • @JohnLedbetter:是的,两个调用者都在做真正的工作。
  • 将锁包装在一个维护“锁定”布尔变量的类中并检查?

标签: c++ linux gcc c++11


【解决方案1】:

严格来说,问题是直接检查std::mutex的锁定性。但是,如果允许将其封装在新类中,则很容易做到:

class mutex :
    public std::mutex
{
public:
#ifndef NDEBUG
    void lock()
    {
        std::mutex::lock();
        m_holder = std::this_thread::get_id(); 
    }
#endif // #ifndef NDEBUG

#ifndef NDEBUG
    void unlock()
    {
        m_holder = std::thread::id();
        std::mutex::unlock();
    }
#endif // #ifndef NDEBUG

#ifndef NDEBUG
    /**
    * @return true iff the mutex is locked by the caller of this method. */
    bool locked_by_caller() const
    {
        return m_holder == std::this_thread::get_id();
    }
#endif // #ifndef NDEBUG

private:
#ifndef NDEBUG
    std::atomic<std::thread::id> m_holder;
#endif // #ifndef NDEBUG
};

注意以下几点:

  1. 在发布模式下,这与 std::mutex 相比具有零开销,除非可能用于构造/销毁(这对于互斥对象来说不是问题)。
  2. m_holder 成员只能在获取互斥体和释放它之间访问。因此,互斥体本身充当m_holder 的互斥体。对 std::thread::id 类型的假设非常弱,locked_by_caller 将正常工作。
  3. 其他 STL 组件,例如,std::lock_guard 是模板,因此它们与这个新类配合得很好。

【讨论】:

  • 不应该是bool locked_by_caller() const { return m_holder == std::this_thread::get_id(); }吗?
  • 是的,应该。我冒昧地编辑了答案(此时仍在等待同行评审)。
  • 要使locked_by_caller 有任何用处,即使您自己没有锁定互斥锁,您也必须能够调用它。所以m_holder 必须是一个原子才能使这段代码合法。
  • 我认为这是最好的解决方案,我也使用类似的东西。为什么没有按照建议将m_holder 类型更新为原子?
  • 我修改了我对进一步思考的评论。这有一些竞争条件,因为锁定/解锁不是通过 locker 的设置自动执行的。确实可靠地回答了“这个线程是否锁定了互斥锁”的问题,这可能是 OP 想要的。它不能可靠地回答“其他线程是否已锁定互斥锁”的问题
【解决方案2】:

std::unique_lock&lt;L&gt; 具有 owns_lock 成员函数(相当于您所说的 is_locked)。

std::mutex gmtx;
std::unique_lock<std::mutex> glock(gmtx, std::defer_lock);

void alpha(void) {
   std::lock_guard<decltype(glock)> g(glock);
   beta(void);
   // some other work
}
void beta(void) {
   assert(glock.owns_lock()); // or just assert(glock);
   // some real work
}

编辑:在此解决方案中,所有锁定操作都应通过 unique_lock glock 而不是“原始”互斥锁 gmtx 执行。例如,alpha 成员函数被重写为lock_guard&lt;unique_lock&lt;mutex&gt;&gt;(或简单地lock_guard&lt;decltype(glock)&gt;)。

【讨论】:

  • 同时从不同线程对unique_lock 调用lock()/unlock() 会导致数据竞争,这使得这毫无用处。
  • std::unique_lock 不是线程安全的,也不打算在多个线程中使用。我见过的实现存储一个布尔值,它是否仅基于lock()unlock() 调用拥有锁,独立于调用线程。因此,将全局std::unique_lock 锁定在一个线程中将使owns_lock() 对于每个 线程都返回true,这是非常不正确的行为。
  • 这不太可行。通常,锁守卫在某些方法中位于堆栈上,而不是类的成员,并且在许多线程中可能有许多锁守卫。我认为 OP 真正想知道的是“这个线程是否锁定了互斥锁?”
【解决方案3】:

您可以只使用recursive_mutex,它可以在同一个线程上多次锁定。注意:如果是我的代码,我会对其进行重组,这样我就不需要recursive_mutex,但它会解决你的问题。

【讨论】:

  • @BasileStarynkevitch:这样看——如果互斥锁可以检测到它是否被这个线程锁定,那么它已经可以实现递归锁定,而不会产生更多的时间开销。因此,具有您想要的属性的假设互斥体不会比使用递归互斥体减慢代码速度。
  • 对 recursive_mutex 的一个警告是它不能用作 condition_variable 的锁,但除此之外它是一个很好的答案。它没有直接回答 OP 的问题——检查我的线程是否已经拥有锁,但它可能避免了坏情况(在不持有锁的情况下更改数据结构)。
【解决方案4】:

试试atomic(例如atomic&lt;bool&gt;atomic&lt;int&gt;),它有一个很好的load 函数,可以做你想做的事情,还有其他很好的函数,比如compare_exchange_strong。

【讨论】:

  • 哇,刚刚结束了两个小时的研究,发现我很久以前就赞成这个答案了……得出了同样的结论。虽然,我开始认为扩展 std::mutex 可能是一个不错的方法......呃,为什么 std::mutex 上没有这个功能?
  • @LimitedAtonement 根据我的经验,我发现 atomic 最简单易用。
  • @LimitedAtonement 另外,如果你想看到一个实际的例子,我有另一个答案(有人刚刚投票所以我才记得它),它可以很好地使用原子,用于异步控制台输入:stackoverflow.com/a/42264216/1599699
  • 这并不能真正回答问题,但我也发现使用原子可以帮助解决这类问题。
  • @Andrew 是的,当然!自旋锁。只要您的等待时间预计很短(例如保护数据结构)或者有很多内核要烧毁,这将工作得很好。我应该说“不会阻塞线程并释放 CPU 资源”。 IIRC 正确,许多互斥体实现在要求操作系统干预之前会进行一些调整。
【解决方案5】:

好吧,如果断言的开销真的不是问题,那么您可以从另一个线程调用try_lock(),保证其行为得到明确定义:

void beta(void) {
  assert(std::async(std::launch::async, [] { return gmtx.try_lock(); })
                 .get() == false &&
         "error, beta called without locking gmtx");
  // some real work
}

【讨论】:

  • 因为如果你从同一个线程锁定两次非递归互斥锁,第二个锁似乎完全阻塞......
  • 虽然不能保证死锁。简直就是UB。
  • 如果调用try_lock的线程已经拥有互斥锁,则为UB。
  • @BrettHale 啊,我明白了。我没有检查规范,我的实现没有死锁。
  • @BasileStarynkevitch 那里,一个具有明确行为的版本。
【解决方案6】:

从技术上讲,这不是一个断言,但我使用了类似的方法来防止对共享状态的解锁访问:在 unsafe 函数(在您的示例中为 beta)上的锁保护类中添加一个引用参数。然后除非调用者创建了锁守卫,否则无法调用该函数。它解决了在锁外意外调用函数的问题,并且它在编译时这样做,没有竞争。

所以,用你的例子:

typedef std::lock_guard<std::mutex> LockGuard;
void alpha(void) {
   LockGuard g(gmtx);
   beta(g);
   // some other work
}

void beta(LockGuard&) {
   // some real work
}

void gamma(void) {
   LockGuard g(gmtx);
   beta(g);
   // some other work
}

//works recursively too
void delta(LockGuard& g)
{
   beta(g);
}

缺点:

  • 不验证锁是否实际包装了正确的互斥体。
  • 需要虚拟参数。在实践中,我通常将这些不安全的函数保密,所以这不是问题。

【讨论】:

    【解决方案7】:

    我的解决方案很简单,使用 try_lock 测试,如果需要解锁:

    std::mutex mtx;
    
    bool is_locked() {
       if (mtx.try_lock()) {
          mtx.unlock();
          return false;
       }
       return true; // locked thus try_lock failed
    }
    

    【讨论】:

    • 这不起作用。 std::mutex 上的 try_lock 当它已经被这个线程锁定时是未定义的,通常会抛出 std::system_exception
    猜你喜欢
    • 2010-12-26
    • 1970-01-01
    • 2012-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-29
    • 2021-10-30
    相关资源
    最近更新 更多