C++中使用锁的try catch

40
在Java中:
Lock lock = new ReentrantLock();
try{
  lock.lock();
  someFunctionLikelyToCauseAnException();
}
catch(e){...}
finally {
  lock.unlock();
}

我的问题是,通过上面的示例,我们知道锁将始终被解锁,因为finally会始终执行,但在C++中有什么保证呢?

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

这将如何运作,为什么会这样?


24
使用std::lock_guard实现RAII。std::lock_guard对象的析构函数会自动解锁其所持有的互斥量。 - Yksisarvinen
7
你应该阅读关于C++中非常重要的一个特性——栈展开,并对其进行了解。 - François Andrieux
16
简而言之,C++不需要finally,因为它有析构函数。 - 463035818_is_not_a_number
6
你在哪里看到Java有析构函数?它有终结器。它被称为不同的名称,因为它不是一个析构函数。问题在于终结器经常被称为析构函数,这让我感到困扰。 - Andrew T Finnell
14
锁通常遵循以下模式:"(1) 状态一致但错误 (2) 进入尝试 (3) 等待直到可以获取锁 (4) 使状态不一致 (5) 使状态正确和一致,(6) 进入 finally (7) 释放锁 (8) 状态现在是一致的和正确的。" 如果在第 4 步和第 5 步之间发生异常会发生什么?我们会直接跳到第 6 步,但此时状态既不一致也错误!然后我们解锁,此时等待中的代码将访问不一致和错误的状态,并且它将崩溃。在 finally 中解锁的这种模式非常危险。 - Eric Lippert
显示剩余10条评论
3个回答

66

为此,我们使用RAII风格构造函数std::lock_guard。当您使用它时

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

lg 会确保无论作用域以什么方式退出(在作用域结束时被销毁),m 都将被解锁,std::lock_guard 的析构函数将调用 unlock

即使抛出异常,堆栈也会被展开 (堆栈展开) 并销毁 lg,从而调用 unlock 以保证锁被释放。


9
如果 OP 没能看到这里实际发生的事情,lg 是一个本地变量。lg(m) 表达式调用了 std::lock_guard 类的构造函数,C++ 保证任何本地变量的析构函数将会在线程退出变量作用域时立即被调用——无论线程如何退出。lock_guard 构造函数锁定给定的锁 m,析构函数解锁它。 - Ohm's Lawman
12
未来的读者请注意:RAII是C++最重要的惯用语之一,描述了C++与Java之间最大的思想差异之一。如果你从Java转向C++,一旦开始充分利用它,你的生活将会变得容易数百倍。请阅读给定链接和这个链接 - user4581301
@BrijendarBakchodia 是的。智能指针是另一个RAII风格的对象。std::lock_guard的构造函数接受互斥量的引用并调用lock。然后当std::lock_guard被销毁时,它的析构函数被调用并调用unlock。就像智能指针在其析构函数中调用delete以确保内存被释放一样。 - NathanOliver
4
首先,通常情况下您不需要在堆上分配互斥锁。如果您确实需要这样做,应该使用智能指针而不是裸指针 newdelete,这样您就可以写成 auto m = std::make_unique<mutex>();。其次,请注意 std::lock_guard 的构造函数 接受一个 std::mutex&。因此,只需通过取消引用将该指针转换为引用:std::lock_guard lg(*m) - Justin
1
@BrijendarBakchodia 确保互斥锁最终被解锁和确保堆分配对象最终被释放是两个不同的问题。即使堆分配的东西恰好是互斥锁,它们也是不同的问题。即使首选解决方案 std::lock_guardstd::unique_ptr 都恰好使用相同的设计模式(RAII),它们也是不同的问题。软件工程的一个重要技能是学会识别和解开出现在同一空间中的不同问题,并保持它们的解决方案分离。 - Ohm's Lawman
显示剩余5条评论

30
C++中的保证与Java中的略有不同。它依赖于自动变量的销毁,即在作用域退出时发生的stack unwinding,当堆栈帧被解开时。无论如何退出作用域,无论是优雅地退出还是由于异常而退出,都会发生这种情况。对于涉及此类锁的场景,首选方法是使用RAII,例如std::lock_guard实现的方式。它持有传递给其构造函数的mutex对象 - 在其中调用mutex的lock()方法,在线程拥有mutex之后 - 并且在作用域退出时进行stack unwinding时,将调用其析构函数 - 在其中调用mutex的unlock()方法,从而释放它。代码将如下所示:
std::mutex m;
{
    std::lock_guard lock(m);
    // Everything here is mutex-protected.
}
// Here you are guaranteed the std::mutex is released.

0

如果在由“lock()”和“unlock()”保护的代码块执行期间抛出异常,这意味着该代码块正在操作的相关对象不再处于有效状态。这可能会或可能不会通过异常触发的堆栈自动展开来回滚,因为在抛出异常之前可能已经发生了一些副作用(例如,通过套接字发送了消息,启动了机器)。此时,更大的问题不是互斥锁是否会被释放(使用lock_guard的唯一保证),而是可能仍然锁定互斥锁是期望的行为,并且可以在调用者清理所有混乱后显式重置。

我的观点是:这不是语言问题。没有语言特性可以保证正确的错误处理。不要将lock_guard和RAII视为银弹。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接