【问题标题】:Cancelling a thread that has a mutex locked does not unlock the mutex取消已锁定互斥锁的线程不会解锁互斥锁
【发布时间】:2012-12-25 10:33:36
【问题描述】:

帮助客户解决他们遇到的问题。我更像是一个系统管理员/DBA 人,所以我正在努力帮助他们。他们说这是内核/环境中的错误,在我坚持认为它存在于他们的代码中或寻求操作系统的供应商支持之前,我试图证明或反驳这一点。

发生在 Red Hat 和 Oracle Enterprise Linux 5.7(和 5.8)上,应用程序是用 C++ 编写的

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

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

  • 服务器程序启动并运行 --> 立即接受连接
  • 服务器程序未运行,机器和网络正常 --> 连接 立即失败,错误“连接被拒绝”
  • 机器或网络崩溃或关闭 --> 连接需要很长时间 失败并出现错误“没有到主机的路由”

问题是取消已锁定互斥锁的线程 (设置清除处理程序以解锁互斥锁)有时不会解锁互斥锁。

这使得主线程在试图锁定互斥锁时挂起。

详细环境信息:

  • 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

代码是用以下方式构建的: c++ -g3 tst2.C -lpthread -o tst2

非常感谢任何建议和指导

【问题讨论】:

  • 你如何用互斥锁取消线程?如果你只是简单地杀死线程,那么析构函数(我假设互斥锁被解锁)很有可能永远不会运行。
  • 大多数时候有人说操作系统有错误,但他们的应用程序没有,他们的应用程序有。这是一种由这种想法带来的自我实现的预言:糟糕的想法,糟糕的代码。如果他们真的只是在冷死地杀死线程,那么当然不会释放互斥锁,也不会运行任何代码来这样做。这是他们自己的设计问题。
  • IIRC,对于 pthreads,无论如何,取消线程本身永远不会释放该线程持有的锁或其他资源 - 必须编写线程以处理特定点的取消并在之前处理必要的清理它消失了......
  • 一种可能是让主线程关闭套接字,而不是取消线程。这将导致另一个线程中的connect(...)EBADF 失败,这可以被适当地检测和处理。
  • “如果一名机械师在维修飞机的过程中死亡,我们如何才能自动让这架飞机重新投入使用?”您只是不要那样做,这不是很明显吗?为什么要取消线程呢?不会有什么害处吧?

标签: c++ c linux mutex


【解决方案1】:

取消的线程不会解锁它们持有的互斥锁是正确的,您需要手动安排该操作,这可能很棘手,因为您需要非常小心地在每个可能的取消点周围使用正确的清理处理程序。假设您使用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_t 上调用pthread_mutexattr_setrobust 创建的。如果一个线程在持有一个健壮的互斥锁时死掉了,内核会记下它,以便下一个试图锁定互斥锁的线程得到特殊的错误代码EOWNERDEAD。如果可能,新线程可以使该线程保护的数据再次保持一致,并取得互斥锁的所有权。这比简单地使用 RAII 类型来锁定和解锁互斥锁要难于正确使用。

一种完全不同的方法是在调用connect 时决定是否真的需要持有互斥锁。在缓慢的操作期间持有互斥锁不是一个好主意。你不能打电话给connect,那么如果成功锁定互斥锁并更新受互斥锁保护的任何共享数据?

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

【讨论】:

  • 是的,RAII 将更可靠地解锁锁。这是否可取是另一回事。锁保护对可能处于不一致状态的对象的访问,并且只有在已知受保护对象处于一致状态时才应该解锁该锁。不言而喻,当异常发生时自动解锁是正确的做法。
  • @Pete,非常好的观点。解锁互斥锁不足以使程序正确。析构函数(和 RAII)也可能用于将数据保持在安全状态,但感谢您提请注意我的答案不完整(顺便说一句,欢迎编辑!)以及 C++ 中的线程取消(尤其是通过异常)是一个棘手的问题,在正确的处理方式上没有达成共识。
  • __cxxabi::__forced_unwind 的行为是否完全像一个正常的异常(除了它被神奇地抛出)?例如,面对线程取消,/* save state to be restored */ try { /* do transaction which transiently involves inconsistent state */ } catch (...) { /* rollback to known good state */ throw; } 之类的代码是否可以正确地进行事务处理?这种取消的实现如何与noexcept 一起工作?编写利用“证明”的代码是错误的,即特定区域的代码不能抛出异常?
  • @bames53,是的,那会很好用。它与其他异常一样工作,只是它可以在任何线程取消点(这基本上意味着 POSIX 取消点不是noexcept)和it must not be swallowed 抛出。如您所料,它与noexcept 交互:如果__forced_unwind 转义noexcept 函数,则调用std::terminate(),因此noexcept 函数实际上是noexcept,它们不会意外抛出一些“特殊”输入
  • 啊,取消积分。现在它是有道理的。 linux 是否支持异步取消,如果支持,那么在这种情况下是否仍然存在?
【解决方案2】:

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

微不足道的修复——不要取消线程。它有什么害处吗?如有必要,让线程检查(当connect 最终完成时)是否仍然需要连接,如果不需要,则关闭它,释放互斥体并终止。您可以使用受互斥体保护的布尔变量来执行此操作。

另外,线程在等待网络 I/O 时不应持有互斥锁。互斥锁只能用于快速且主要受 CPU 限制或可能受本地磁盘限制的事物。

最后,如果您觉得需要从外部伸手并迫使线程做某事,请退后一步。 为那个线程写了代码。如果您觉得需要,这意味着您没有编写该线程来执行您真正希望它执行的操作。解决方法是修改线程以执行您真正想要的,并且只执行您真正想要的。这样你就不必从外面“推它”了。

【讨论】:

  • 如果它持有互斥锁直到它最终超时,那么如果它阻止新线程重试操作或其他一些替代操作,它就会造成伤害(或至少导致问题)。正如您所说,避免这种伤害的最佳方法是避免在此类操作中持有锁,并且首先不要以这种方式对其进行编码。我想我更喜欢你的“那就不要那样做”的答案,而不是我自己的答案。
  • @JonathanWakely 我同意,-1 表示您的回答。 (我的孩子)
猜你喜欢
  • 2018-05-23
  • 1970-01-01
  • 2021-12-23
  • 2010-11-22
  • 2022-07-31
  • 1970-01-01
  • 2013-01-31
  • 2010-09-16
相关资源
最近更新 更多