【问题标题】:Is std::mutex trivially destructible?std::mutex 可以轻易破坏吗?
【发布时间】:2018-03-17 22:15:45
【问题描述】:

我有一个函数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&lt;std::mutex&gt;::value 返回 true,所以至少在这个编译器中看起来没问题。有明确的答案吗?

相关:Static and Global Variables 上的 Google C++ 样式指南


编辑

显然,我不够清楚,应该提供更多上下文。是的,根本问题是我希望barm 之前被破坏。这是众所周知的“静态初始化惨败问题”的“破坏”部分,例如:

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

要点很简单:如果有任何其他静态对象 析构函数可能会在 ans 被破坏后使用 ans,砰,你死定了。 如果a、b、c的构造函数使用ans,一般应该没问题 因为运行时系统将在静态反初始化期间, destruct ans 在这三个对象中的最后一个被破坏之后。 但是,如果 a 和/或 b 和/或 c 未能在其构造函数中使用 ans 和/或如果任何地方的任何代码获得 ans 的地址并将其交给 其他一些静态对象,所有的赌注都没有了,你必须非常, 非常小心。

这就是为什么 Google 不建议使用静态对象的原因,除非它们很容易被破坏。问题是,如果对象可以简单地破坏,那么破坏的顺序并不重要。即使mbar 之前被“破坏”,您仍然可以在bar 的析构函数中使用m 而不会导致程序崩溃,因为析构函数实际上什么都不做(它不会释放任何内存或释放任何其他类型的资源)。

事实上,如果m 可以简单地破坏,那么程序甚至可能根本无法破坏m,这实际上确保mbar 或任何其他静态对象之后被“破坏”不可轻易破坏。例如:

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

程序不需要调用对象的析构函数来结束 如果对象是可简单破坏的或者程序的生命周期 不依赖析构函数的副作用。

由于这些原因,如果您的单例可以简单地破坏,则使用复杂的单例习语(例如 Nifty Counter idiom)实际上是矫枉过正。

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

相关:

【问题讨论】:

  • 这句话值得关注“被普通可破坏对象占用的存储空间可以在不调用析构函数的情况下被重用。”来自:en.cppreference.com/w/cpp/types/is_destructible猜这取决于实现是否需要/需要释放native_handle
  • Related。似乎暗示“不”。
  • 标准说是is_destructable,而不是is_trivially_destructable。看起来更轻松了。
  • 我不明白你的意思。平凡破坏冲击破坏秩序如何?静态持续时间销毁顺序与其构造顺序相反。
  • 至于你的问题,一个确定的答案here

标签: c++ static mutex


【解决方案1】:

标准是怎么说的

答案是“否”:根据C++17 Standardstd::mutex 类型不需要有一个平凡的析构函数。互斥类型的一般要求在 [thread.mutex.requirements] 中有描述,唯一描述可破坏性的段落如下:

互斥锁类型应为 DefaultConstructible 和 Destructible。如果 互斥锁类型对象的初始化失败,异常 应抛出类型 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";
}

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

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

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

我相信在一些 pthread 实现上是一个安全的改变。主要的 “pthread_mutex_destroy”的目的是将锁设置为无效 值,从而可以诊断出use-after-free。 AFAIK 互斥体 用“PTHREAD_MUTEX_INITIALIZER”初始化没有资源,所以 省略调用不会导致泄漏。

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

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

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

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

我该怎么办?

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

最简单的方法是有目的地“泄露”一个动态分配的本地静态互斥体,就像这样,它既快速又很可能是安全的:

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

否则,更简洁的方法是使用Nifty Counter idiom(或“Schwarz Counter”)来控制互斥体的生命周期,但请注意,这种技术会在程序启动和终止时引入少量开销。

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 感谢您的回答(有关 VC++ 行为的信息)。但是,正如您可以想象的那样,我不能将它们放在同一个翻译单元中(Bar 只是foo() 的众多客户之一,可能在不同的共享库中)。似乎我们需要 Nifty Counter 成语或 Trusty Leaky 成语用于静态互斥锁,至少在某些平台上是这样。
    • 我接受了这个答案,因为它确实回答了这个问题,尽管如果你能包含引用来支持答案会很好。这可以是标准的摘录、实施说明或测试,例如由@liilescent:wandbox.org/permlink/iEqWJHKmRHXAqoZo 提供。如果/当我找到时间和勇气时,我可能会自己写另一个更完整的答案;)
    • 谢谢。我会在睡一会后研究它。额外评论:我突然想到,在编程​​词典中,“成语”是一个成语!也就是说,它是一个无法从常见的定义和语法中确定含义的术语。如果一个新手在字典中查找“成语”,他无法说出在编程世界中它的意思是“一种有点棘手的模式”。
    • 哈哈,我想我以前从来没有看过字典中“成语”的定义,我想我只是在编程上下文中使用过它;)
    • 太棒了。总的来说,我会使用成语,直到我放弃为止。但这既不是这里也不是那里。我今天带了伞,因为下雨了。为什么我的伞下雨了,我永远不会知道。
    猜你喜欢
    • 2021-05-17
    • 1970-01-01
    • 2017-03-28
    • 2013-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-24
    • 2016-10-22
    相关资源
    最近更新 更多