std::mutex 是微不足道的可破坏的吗?

Is std::mutex trivially destructible?

我有一个函数 foo() 受互斥量 m 保护,它被定义为 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 returns true,所以至少在这个编译器中看起来没问题。有确定的答案吗?

相关:Google Static and Global Variables

上的 C++ 风格指南

编辑

显然,我不够清楚,应该提供更多上下文。是的,潜在的问题是我希望 barm 之前被销毁。这是众所周知的 "static initialization fiasco problem" 的 "destruction" 部分,例如参见:

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

The point is simple: if there are any other static objects whose destructors might use ans after ans is destructed, bang, you’re dead. If the constructors of a, b and c use ans, you should normally be okay since the runtime system will, during static deinitialization, destruct ans after the last of those three objects is destructed. However if a and/or b and/or c fail to use ans in their constructors and/or if any code anywhere gets the address of ans and hands it to some other static object, all bets are off and you have to be very, very careful.

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

事实上,如果 m 是微不足道的可破坏的,那么程序甚至可能根本不会破坏 m,这实际上确保了 m 在之后是 "destructed" bar 或任何其他不易破坏的静态对象。参见实例:

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

A program is not required to call the destructor of an object to end its lifetime if the object is trivially-destructible or if the program does not rely on the side effects of the destructor.

由于这些原因,如果您的单例是微不足道的可破坏的,那么使用诸如 Nifty Counter idiom 之类的复杂单例习语实际上是过大的杀伤力。

换句话说,如果 std::mutex 是微不足道的可破坏的,那么我上面的代码示例是安全的:m 要么在 bar 之后被破坏,要么是 "technically destructed"在 bar 之前,但无论如何都不会导致崩溃。但是,如果 std::mutex 不是微不足道的可破坏的,那么我可能需要使用 Nifty Counter 惯用法,或者更简单但 "purposefully leaking" Trusty Leaking idiom.

相关:

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

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

标准怎么说

答案是 "no":根据 C++17 Standard,类型 std::mutex 不需要具有平凡的析构函数。 [thread.mutex.requirements] 中描述了互斥锁类型的一般要求,唯一描述可破坏性的段落如下:

The mutex types shall be DefaultConstructible and Destructible. If initialization of an object of a mutex type fails, an exception of type system_error shall be thrown. The mutex types shall not be copyable or movable.

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

The class mutex shall satisfy all of the mutex requirements (33.4.3). It shall be a standard-layout class (Clause 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";
}

换句话说,目前似乎只有 Linux 平台上的 GCC 为 std::mutex.

提供了一个简单的析构函数

但是,请注意 Bug Request 使 std::mutex 在某些平台上的 Clang 中可以轻易破坏:

For these reasons I believe we should change 'std::mutex' to be trivially destructible (when possible). This means NOT invoking "pthread_mutex_destroy(...)" in the destructor.

I believe is a safe change on some pthread implementations. The main purpose of "pthread_mutex_destroy" is to set the lock to an invalid value, allowing use-after-free to be diagnosed. AFAIK mutex's initialized with "PTHREAD_MUTEX_INITIALIZER" own no resources and so omitting the call will not cause leaks.

On other pthread implementations this change will not be possible.

一条 follow-up 消息详细说明可以进行此更改的平台似乎包括 NPTL (GLIBC) 和 Apple,而在 FreeBSD 上似乎不可能。

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

A trivial destructor is important for similar reasons. If a mutex is used during dynamic initialization it might also be used during program termination. If a static mutex has a non-trivial destructor it will be invoked during termination. This can introduce the "static deinitialization order fiasco".

我该怎么办?

如果您需要可移植代码中的全局互斥体(例如保护另一个全局对象,如内存池等),并且处于可能受制于 "static deinitialization order fiasco" 的用例中,那么你需要使用谨慎的单例技术来确保互斥量不仅在第一次使用前创建,而且在最后一次使用后销毁(或根本不销毁)。

最简单的方法是有目的地 "leak" 一个动态分配的本地静态互斥体,就像这样,它速度快而且很可能是安全的:

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

否则,更简洁的方法是使用 Nifty Counter idiom(或 "Schwarz Counter")来控制互斥量的生命周期,尽管要注意这种技术在启动和终止时引入了一个小的开销程序。