取消一个已经锁定互斥的线程不会解锁该互斥。

12

我正在帮助一个客户解决他们遇到的问题。由于我更专注于系统管理员/数据库管理方面,所以我在帮助他们时感到有些吃力。他们说这是内核/环境中的一个bug,我正试图证明或证伪这一点,然后才会坚持认为这是他们的代码问题或者寻求厂商对操作系统的支持。

在Red Hat和Oracle Enterprise Linux 5.7(和5.8)上出现了这个问题,应用程序是用C++编写的。

他们遇到的问题是主线程启动一个单独的线程来执行可能运行时间很长的TCP connect() [客户端连接到服务器]。如果'长时间运行'方面太长,他们会取消该线程并启动另一个线程。

之所以这样做是因为我们不知道服务器程序的状态:

  • 服务器程序正在运行-->立即接受连接
  • 服务器程序没有运行,机器和网络都OK-- >立即失败,出现错误“connection refused”
  • 机器或网络崩溃或关闭-->连接需要很长时间才能失败,并出现错误“no route to host”

问题在于取消已锁定互斥量的线程(使用清理处理程序设置为解锁互斥量)有时不会解锁互斥量。

这让主线程在尝试锁定互斥量时挂起。

详细的环境信息:

  • glibc-2.5-65
  • glibc-2.5-65
  • libcap-1.10-26
  • kernel-debug-2.6.18-274.el5
  • glibc-headers-2.5-65
  • glibc-common-2.5-65
  • libcap-1.10-26
  • kernel-doc-2.6.18-274.el5
  • kernel-2.6.18-274.el5
  • kernel-headers-2.6.18-274.el5
  • glibc-devel-2.5-65

代码是用以下方式构建的:

-g3 tst2.C -lpthread -o tst2

非常感谢任何建议和指导。


2
你是如何使用互斥锁取消线程的?如果你仅仅杀死线程,很有可能析构函数(我假设在其中互斥锁被解锁)将永远不会运行。 - crush
4
大多数情况下,当有人说操作系统有bug但他们的应用程序没有bug时,实际上他们的应用程序是有错误的。这是一种自我实现的预言,由这种想法引起:糟糕的思维会导致糟糕的代码。如果他们真的只是让线程直接退出,那么互斥锁将不会被释放,也就不会执行任何代码来释放它。这是他们自己的设计问题。 - GManNickG
4
如果我没记错的话,就pthread而言,取消一个线程本身不会释放由该线程持有的锁或其他资源——线程必须在特定点处理取消并在离开之前进行必要的清理... - twalberg
4
一种可能的解决方案是让主线程关闭套接字,而不是取消线程。这将导致其他线程中的 connect(...) 失败并出现 EBADF 错误,可以检测并进行适当处理。 - clstrfsck
3
“如果机械师在维修飞机的过程中去世了,我们应该如何自动地使这架飞机重新进入服务状态?” 这不是很明显吗?你只需要不要这么做。那么为什么还要取消这个讨论串呢?它又没有造成任何伤害,不是吗? - David Schwartz
显示剩余4条评论
2个回答

17

取消的线程不会自动释放其持有的互斥锁,您需要手动安排释放操作。这可能比较棘手,因为您需要非常小心地在每个可能的取消点周围使用正确的清理处理程序。假设您正在使用 pthread_cancel 取消线程,并使用 pthread_cleanup_push 设置清理处理程序以释放互斥锁,则有几种替代方法可以尝试,这些方法可能更容易正确实现,因此可能更可靠。

使用 RAII 解锁互斥锁会更加可靠。在GNU/Linux上,pthread_cancel 是使用特殊类型 __cxxabi::__forced_unwind 实现的,因此当线程被取消时,会抛出异常并解开堆栈。如果一个 RAII 类型锁定了互斥锁,那么如果堆栈被 __forced_unwind 异常解开,它的析构函数将被保证运行。Boost Thread 提供了一个便携式的 C++ 库,它包装了 Pthreads 并且更易于使用。它提供了一个 RAII 类型 boost::mutex 和其他有用的抽象。Boost Thread 还提供了自己的“线程中断”机制,类似于 Pthread 取消但不完全相同,并且 Pthread 取消点(如 connect)不是 Boost Thread 中断点,这对一些应用程序可能有帮助。然而,在您的客户端情况下,由于取消点的目的是中断 connect 调用,他们可能确实需要坚持使用 Pthread 取消。GNU/Linux 实现取消作为异常的(非便携式)方式意味着它将与 boost::mutex 很好地配合使用。
在使用C++编程时,没有明确锁定和解锁互斥量的借口。在我看来,C++最重要和最有用的功能是析构函数,它们非常适合自动释放资源,例如互斥锁。

另一个选择是使用鲁棒互斥锁,通过在初始化互斥锁之前调用pthread_mutexattr_setrobust来创建。如果一个线程在持有鲁棒互斥锁时死亡,内核会做出记录,以便下一个尝试锁定互斥锁的线程获取特殊的错误代码EOWNERDEAD。如果可能,新线程可以再次使由线程保护的数据一致,并拥有互斥锁的所有权。这比简单地使用RAII类型来锁定和解锁互斥锁要难得多。

完全不同的方法是决定是否需要在调用connect时保持互斥锁。在缓慢操作期间保持互斥锁并不是一个好主意。你不能先调用connect然后如果成功则锁定互斥锁并更新由互斥锁保护的任何共享数据吗?

我的偏好是既使用Boost Thread,又避免长时间持有互斥锁。


4
是的,RAII将更可靠地解锁锁。是否需要这样做是另一回事;该锁保护着可能处于不一致状态的对象的访问,只有在已知受保护对象处于一致状态时才应解锁该锁。自动解锁锁是否为正确操作并不明显,当异常发生时。 - Pete Becker
@Pete,非常好的观点。解锁互斥锁是不足以使程序正确的。析构函数(和RAII)也可以潜在地用于使数据处于安全状态,但感谢你注意到了我的回答不完整(欢迎编辑!),并且C++中的线程取消(特别是通过异常)是一个棘手的问题,没有共识应该如何处理它。 - Jonathan Wakely
__cxxabi::__forced_unwind 行为是否完全像普通异常一样(除了它被神奇地抛出)?例如,像下面的代码是否可以在线程取消时正确处理事务: /* 保存要恢复的状态 */ try { /* 进行短暂涉及不一致状态的交易 */ } catch (...) { /* 回滚到已知的良好状态 */ throw; }?这种取消的实现与 noexcept 如何协同工作?编写利用 'proofs' 的代码是否有问题,即某个代码区域不会抛出异常? - bames53
@bames53,是的,那样做可以。它的工作方式类似于其他异常,只不过它可以在任何线程取消点抛出(这基本上意味着 POSIX 取消点不是“noexcept”),并且不能被吞噬。它与 noexcept 的交互方式符合您的预期:如果 __forced_unwind 逃逸了 noexcept 函数,则调用 std::terminate(),因此 noexcept 函数确实是 noexcept,它们不会意外地抛出某些“特殊”类型。 - Jonathan Wakely
啊,取消点。现在有意义了。Linux是否支持异步取消,如果是这样的话,那么所有这些仍然适用吗? - bames53
我相信它支持,但我不知道如何或是否涉及__forced_unwind。 我的猜测是异步取消也使用该异常类型...尽管显然这很难正确处理,这在异步取消中总是正确的。 - Jonathan Wakely

4
他们遇到的问题是主线程启动一个单独的线程去执行潜在的长时间运行的TCP connect()操作 [客户端连接到服务器]。如果“长时间运行”花费太长时间,他们会取消该线程并启动另一个线程。
修复方法很简单 -- 不要取消线程。这样做会有什么问题吗?如果必要,在connect最终完成时,让线程检查是否仍需要该连接,并在不需要时关闭它、释放互斥锁并终止该线程。您可以使用由互斥锁保护的布尔变量来实现此操作。
另外,线程在等待网络I/O时不应持有互斥锁。互斥锁应仅用于快速且主要受CPU限制或可能受本地磁盘限制的事物。
最后,如果您觉得需要从外部强制线程执行某些操作,请退后一步。是您为该线程编写了代码。如果您感到需要这样做,这意味着您没有编写该线程以执行您真正想要的操作。解决方法是修改线程,使其仅执行您实际需要的操作。然后,您就不必再从外部“推它”。

如果它一直持有互斥锁直到最终超时,那么它会造成伤害(或至少引起问题),如果它阻止了新线程重试操作或其他替代操作。避免这种伤害的最好方法就是像你所说的那样,在这些操作中避免持有锁,并且一开始就不要编写这样的代码。我认为我更喜欢你的“那就别这么做”的答案,而不是我的答案。 - Jonathan Wakely
@JonathanWakely 我同意,你的回答得-1分。(开玩笑) - Yakk - Adam Nevraumont

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