【问题标题】:Try catch with locks in C++尝试使用 C++ 中的锁进行捕获
【发布时间】:2019-02-11 21:11:56
【问题描述】:

在 Java 中:

Lock lock = new ReentrantLock();
try{
  lock.lock();
  someFunctionLikelyToCauseAnException();
}
catch(e){...}
finally {
  lock.unlock();
}

我的问题是,对于上面这个例子,我们知道锁总是会被解锁,因为 finally 总是会执行,但是 C++ 的保证是什么?

mutex m;
m.lock();
someFunctionLikelyToCauseAnException();
/// ????

这将如何工作以及为什么?

【问题讨论】:

  • 使用 std::lock_guard 进行 RAII 方法。 std::lock_guard 对象将解锁它在析构函数中的任何互斥锁。
  • 你应该阅读stack unwinding,这是c++的一个非常重要的特性。
  • 简而言之:c++不需要finally,因为它有析构函数
  • @user4581301 你从哪里读到Java有析构函数?它有终结器。它被称为不同的东西,因为它不是析构函数。问题是终结器通常被称为析构函数,我对此有疑问。
  • 锁通常与模式一起使用:“(1)状态一致但错误(2)进入尝试(3)等到你可以拿锁(4)使状态不一致(5 )使状态正确且一致,(6)进入最终(7)释放锁(8)状态现在一致且正确。” 如果在第 4 步之后和第 5 步之前发生 throw 会发生什么? 我们直接进入第 6 步,但状态现在既不一致又错误!然后我们解锁锁,现在等待的代码可以访问不一致的错误状态,然后崩溃。这种在 finally 中解锁的模式是超级危险的

标签: c++ multithreading operating-system locking


【解决方案1】:

C++ 的保证是什么?

与您在 Java 中提到的保证相比,C++ 中的相关保证有点不同。它依赖于在作用域退出时发生的自动变量的销毁,而不是 finally 块,因为堆栈帧被展开。不管范围是如何退出的,无论是优雅地还是由于异常,都会出现这个stack unwinding

关于此类的场景的首选方法是使用RAII,例如由std::lock_guard 实现。它持有传递给其构造函数的mutex 对象——在该对象内部调用mutexlock() 方法,之后线程拥有互斥锁——并且在堆栈展开处作用域的出口调用了它的析构函数——在其中调用mutexunlock()方法,从而释放它。

代码将如下所示:

std::mutex m;
{
    std::lock_guard lock(m);
    // Everything here is mutex-protected.
}
// Here you are guaranteed the std::mutex is released.

【讨论】:

    【解决方案2】:

    为此,我们使用RAII-style 构造std::lock_guard。当你使用

    std::mutex m;
    { // start of some scope
        std::lock_guard lg(m);
        // stuff
    } // end of scope
    

    lg 将确保 m 将被解锁,无论作用域在哪个路径下,因为它在作用域退出时被销毁,std::lock_guards 析构函数将调用unlock

    即使抛出异常,堆栈也会被解除 (stack unwinding),并且该进程会破坏 lg,而 lg 又会调用 unlock,以保证释放锁。

    【讨论】:

    • 以防 OP 看不到这里真正发生了什么; lg 是一个局部变量。 lg(m) 表达式调用std::lock_guard 类的构造函数,C++ 保证当线程从变量的范围——不管线程如何退出。 lock_guard 构造函数锁定给定的锁m,而析构函数将其解锁。
    • 未来读者请注意:RAII 是 C++ 最重要的习语之一,描述了 C++ 和 Java 之间最大的意识形态差异之一。如果你从 Java 转向 C++,一旦你开始利用它,你的生活将会轻松数百倍。阅读给定的链接和this one.
    • 所以lock_guard 基本上就像智能指针一样包装了互斥锁m
    • @BrijendarBakchodia 首先,您通常不需要在堆上分配互斥锁。如果你这样做了,你应该使用智能指针而不是原始的newdelete,所以你会写它auto m = std::make_unique<mutex>(); 其次,注意std::lock_guard's constructor 需要一个std::mutex&。因此,只需通过取消引用将该指针转换为引用:std::lock_guard lg(*m)
    • @BrijendarBakchodia 确保互斥体最终被解锁并确保堆分配的对象最终被释放是两个独立的问题。即使堆分配的东西恰好是互斥体,它们也是单独的问题。尽管首选解决方案std::lock_guardstd::unique_ptr 碰巧使用相同的设计模式 (RAII),但它们是不同的问题。软件工程的一项重要技能是学习识别和解开出现在同一空间中的不同问题,并将它们的解决方案分开。
    【解决方案3】:

    如果在执行受临界区保护的一段代码(即“lock()”和“unlock()”之间的代码)期间抛出异常,则表示该段代码正在操作的关联对象不再处于有效状态。这可能会或可能不会通过由异常触发的堆栈的自动展开来回滚,因为在抛出异常之前可能已经发生了一些副作用(一条消息已通过套接字发送,一个例如,机器已启动)。在这一点上,这里更大的问题不是互斥锁是否会被释放(使用 lock_guard 的唯一保证)。在某些情况下,互斥锁仍然被锁定是理想的行为,并且可以在调用者清理所有混乱后显式重置。

    我的意思是:这不是语言问题。没有语言功能可以保证正确的错误处理。不要把 lock_guard 和 RAII 作为灵丹妙药。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-04-07
      • 2015-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-05
      相关资源
      最近更新 更多