我有一个由互斥foo()
保护的函数m
,它被定义为foo()
的局部静态变量。我想知道在具有静态存储持续时间的对象foo()
的析构函数中调用bar
是否安全:
// 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
,所以至少在这个编译器中似乎没问题。任何明确的答案?
相关:Static and Global Variables
上的Google C ++样式指南修改
显然,我不够清楚,应该提供更多背景信息。是的,根本问题是我希望在bar
之前销毁m
。这是&#34;破坏&#34;众所周知的静态初始化惨败问题&#34;的一部分,例如:
https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2
重点很简单:如果有任何其他静态对象 在ans被破坏之后,析构函数可能会使用ans,砰,你已经死了。 如果a,b和c的构造函数使用ans,那么通常应该没问题 因为在静态取消初始化期间运行时系统会 在这三个对象的最后一个被破坏之后破坏ans。 但是,如果a和/或b和/或c未能在其构造函数中使用ans 和/或如果任何代码在任何地方获得ans的地址并交给它 一些其他静态物体,所有的赌注都关闭,你必须非常, 非常小心。
这就是为什么Google建议不要使用静态对象,除非它们是可以轻易破坏的。问题是,如果物体是可以轻易破坏的,那么破坏的顺序并不重要。即使m
已被破坏&#34;在bar
之前,您仍然可以在m
的析构函数中使用bar
而不会导致程序崩溃,因为析构函数实际上什么都不做(它不会解除分配任何内存或释放)任何其他类型的资源)。
事实上,如果m
是可以轻易破坏的,那么该程序甚至可能根本不会破坏m
,这实际上可以确保m
被破坏&#34;在bar
之后或任何其他不易破坏的静态目标。例如,见:
http://en.cppreference.com/w/cpp/language/lifetime#Storage_reuse
不需要程序来调用对象的析构函数来结束 它的生命周期,如果对象是简单的可破坏的或程序 不依赖于析构函数的副作用。
由于这些原因,如果您的单身人士可以轻易破坏,那么使用复杂的单身惯用法(如Nifty Counter idiom)实际上是过分的。
换句话说,如果std::mutex
是可以轻易破坏的,那么我上面的代码示例是安全的:m
要么在bar
之后被破坏,要么在技术上被破坏&#34; 34;在bar
之前,但无论如何都不会导致崩溃。但是,如果std::mutex
不是轻易破坏的话,那么我可能需要使用Nifty Counter习惯用法,或者更简单但有目的地泄露&#34; Trusty Leaking idiom
相关:
答案 0 :(得分:2)
在VC ++上,std :: mutex并非简单易破坏,所以你的问题的答案是否定的。
我真正想知道的是如何确保在Bar bar
的析构函数之前调用foo::m
的析构函数。好吧,除非他们在同一个翻译单元,否则你不能这样做。如果你在一个名为foobar.cpp的文件中定义它们并在条形栏上面定义foo(),你就会很好。
答案 1 :(得分:2)
答案是“不”:根据C++17 Standard,类型std::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";
}
false
true
false
换句话说,目前似乎只有Linux平台上的GCC为std::mutex
提供了一个简单的析构函数。
但是,请注意在某些平台上有一个Bug Request使std::mutex
在Clang中可以轻易破坏:
由于这些原因,我认为我们应该改变'std :: mutex' 可以轻易破坏(如果可能的话)。这意味着 NOT 调用 析构函数中的“pthread_mutex_destroy(...)”。
我认为对某些pthread实现是一个安全的改变。主要的 “pthread_mutex_destroy”的目的是将锁定为无效 价值,允许使用后免费诊断。 AFAIK互斥体 用“PTHREAD_MUTEX_INITIALIZER”初始化没有资源等等 省略呼叫不会导致泄密。
在其他pthread实现上,这种改变是不可能的。
后续消息详细说明可以进行此更改的平台似乎包括NPTL(GLIBC)和Apple,而在FreeBSD上似乎不可能。
请注意,错误请求还提到我在问题中提到的问题(强调我的):
普通的析构函数很重要出于类似的原因。如果是互斥量 在动态初始化期间使用它也可能在使用期间使用 程序终止。如果静态互斥锁有一个非平凡的析构函数呢 将在终止期间被调用。 这可以引入“静态 deinitialization order fiasco“。
如果您需要可移植代码中的全局互斥锁(例如,为了保护其他全局对象,例如内存池等),并且处于可能受"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”)来控制互斥锁的生命周期,但要注意这种技术在启动和终止程序时会产生很小的开销。