标准::互斥体是微不足道的可破坏的

Is std::mutex trivially destructible?

本文关键字:微不足道 可破坏 标准      更新时间:2023-10-16

我有一个受互斥m保护foo()函数,该互斥锁被定义为foo()的局部静态变量。我想知道在具有静态存储持续时间bar对象的析构函数中调用foo()是否安全:

// foo.h
void foo();
// foo.cpp
#include "foo.h"
#include <mutex>
void foo()  {
static std::mutex m;
std::lock_guard<std::mutex> lock(m);
// ...
}
// bar.h
struct Bar { ~Bar(); };
extern Bar bar;
// bar.cpp
#include "bar.h"
#include "foo.h"
Bar::~Bar() { foo(); }
Bar bar;
// main.cpp
int main() {
Bar bar;
return 0;
}

如果std::mutex是微不足道的可破坏的,这应该是安全的,因为bar将在m之前被破坏。在 GCC 5.4 上,Ubuntu 16.04 调用std::is_trivially_destructible<std::mutex>::value返回true,所以至少在这个编译器中看起来还可以。有什么明确的答案吗?

相关:谷歌C++静态和全局变量风格指南


编辑

显然,我不够清楚,应该提供更多的背景。是的,根本问题是我希望barm之前被破坏。这是众所周知的"静态初始化惨败"的"破坏"部分,参见示例:

https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2

要点很简单:如果还有其他静态对象 析构函数可能会在 ANS 被销毁后使用 ANS,砰,你死了。 如果 a、b 和 c 的构造函数使用 ans,通常应该没问题 由于运行时系统将在静态取消初始化期间, 在这三个对象中的最后一个被销毁后销毁 ans。 但是,如果 a 和/或 b 和/或 c 在其构造函数中无法使用 ans 和/或如果任何地方的任何代码获取 ans 的地址并将其交给 其他一些静态对象,所有赌注都关闭了,你必须非常, 非常小心。

这就是为什么谷歌建议不要使用静态对象,除非它们是微不足道的可破坏的。问题是,如果物体是微不足道的可破坏的,那么破坏的顺序并不重要。即使mbar之前被"析构",您仍然可以在实践中在bar的析构函数中使用m而不会使程序崩溃,因为析构函数实际上什么都不做(它不会释放任何内存或释放任何其他类型的资源)。

事实上,如果m是微不足道的可破坏的,那么程序甚至可能根本不会破坏m,这实际上确保了mbar或任何其他静态物体之后被"破坏",这些物体不是平凡可破坏的。例如,请参阅:

http://en.cppreference.com/w/cpp/language/lifetime#Storage_reuse

不需要程序来调用对象的析构函数来结束 如果对象是微不足道的可破坏的,或者如果程序是可破坏的,则其生存期 不依赖于析构函数的副作用。

由于这些原因,如果您的单例是微不足道的可破坏的,那么使用复杂的单例习语(例如 Nifty Counter 习语)实际上是矫枉过正的。

换句话说,如果std::mutex是微不足道的可破坏的,那么我上面的代码示例是安全的:m要么在bar后被破坏,要么在bar之前被"技术上破坏",但无论如何都不会导致崩溃。但是,如果std::mutex不是微不足道的可破坏的,那么我可能需要使用 Nifty Counter 习语,或者使用更简单但"故意泄漏"的 Trusty Leaking 成语。

相关:

  • https://stackoverflow.com/a/335746/1951907
  • https://stackoverflow.com/a/17712497/1951907

标准怎么说

答案是否定的:根据 C++17 标准,std::mutex型不需要具有微不足道的析构函数。[thread.mutex.requirements] 中描述了互斥类型的一般要求,描述可破坏性的唯一段落如下:

互斥锁类型应为"默认可构造"和"可破坏"。如果 初始化互斥锁类型的对象失败,异常 应抛出system_error型。互斥锁类型不得 可复制或可移动。

稍后,[thread.mutex.class] 部分特别详细介绍了std::mutex,但除了以下段落外,没有指定其他要求:

类互斥锁

应满足所有互斥锁要求(33.4.3)。 它应为标准布局类(第12条)。

但是,请注意,在所有互斥锁类型中,std::mutex是唯一具有constexpr构造函数的类型,这通常暗示该类型也可能是微不足道的可破坏的。

编译器怎么说

(感谢@liliscent创建测试)

#include <iostream>
#include <type_traits>
#include <mutex>
using namespace std;
int main()
{
std::cout << boolalpha << is_trivially_destructible<mutex>::value << "n";
}
  • 使用 Clang 运行 7.0.0:false
  • 使用 GCC 8.0.1 运行:true
  • 使用 MSVC 版本 19.00.23506 运行:false

换句话说,目前似乎只有Linux平台上的GCC为std::mutex提供了一个微不足道的析构函数。

但是,请注意,在某些平台上,有一个错误请求可以使std::mutex在Clang中变得微不足道的可破坏:

出于这些原因,我认为我们应该将"std::mutex"更改为 微不足道的可破坏性(如果可能)。这意味着调用 析构函数中的"pthread_mutex_destroy(...)"。

我相信这是对某些 pthread 实现的安全更改。主要 "pthread_mutex_destroy"的目的是将锁设置为无效 值,允许诊断释放后使用。AFAIK 互斥锁 初始化为"PTHREAD_MUTEX_INITIALIZER"没有资源等 省略调用不会导致泄漏。

在其他 pthread 实现上,此更改将是不可能的。

后续消息详细说明了可能进行此更改的平台似乎包括NPTL(GLIBC)和Apple,而在FreeBSD上似乎是不可能的。

请注意,错误请求还提到了我在问题中提到的问题(强调我的):

出于类似的原因,一个微不足道的析构函数很重要。如果互斥锁 在动态初始化期间使用 它也可以在 程序终止。如果静态互斥锁具有非平凡析构函数,则 将在终止期间调用。这可以引入"静态" 取消初始化顺序惨败"。

我该怎么办?

如果你在可移植代码中需要一个全局互斥锁(例如保护另一个全局对象,如内存池等),并且处于可能受到"静态解初始化顺序惨败"影响的用例中,那么你需要使用谨慎的单例技术来确保互斥锁不仅在首次使用之前创建, 但在最后一次使用后也会被破坏(或根本没有被破坏)。

最简单的方法是有目的地"泄漏"动态分配的本地静态互斥锁,如下所示,这既快速又很可能是安全的:

void foo() {
static std::mutex* m = new std::mutex;
std::lock_guard<std::mutex> lock(*m);
// ...
}

否则,更简洁的方法是使用 Nifty 计数器习惯用语(或"Schwarz 计数器")来控制互斥锁的生存期,但请注意,此技术在程序启动和终止时引入了少量开销。

在 VC++ 上 std::mutex 不是微不足道的可破坏的,所以你的问题的答案是否定的。

(我认为)您真正想知道的是如何确保在foo::m的析构函数之前调用Bar bar析构函数。 好吧,除非它们在同一个翻译单元中,否则您不能。如果你在一个名为 foobar 的文件中定义它们.cpp并在 Bar bar 上方定义 foo(),那就很好了。