Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is std::mutex trivially destructible?

Tags:

c++

static

mutex

I have a function foo() protected by a mutex m which is defined as a local static variable of foo(). I'd like to know whether it's safe to call foo() in the destructor of an object bar with static storage duration:

// 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;
}

If std::mutex is trivially destructible, this should be safe, because bar will be destructed before m. On GCC 5.4, Ubuntu 16.04, calling std::is_trivially_destructible<std::mutex>::value returns true, so it seems okay at least in this compiler. Any definitive answer?

Related: Google C++ Style Guide on Static and Global Variables


Edit

Apparently, I wasn't clear enough and should have provided more context. Yes, the underlying problem is that I want bar to be destructed before m. This is the "destruction" part of the well known "static initialization fiasco problem", see for example:

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.

This is why Google recommends against using static objects, unless they are trivially destructible. The thing is, if the object is trivially destructible, then the order of destruction doesn't really matter. Even if m is "destructed" before bar, you can still in practice use m in the destructor of bar without crashing the program, because the destructor effectively does nothing (it doesn't deallocate any memory or release any other type of resources).

And in fact, if m is trivially destructible, then the program may not even destruct m at all, which actually ensures that m is "destructed" after bar or any other static objets which are not trivially destructible. See for instance:

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.

For these reasons, it is in practice overkill to use complex singleton idioms such as the Nifty Counter idiom if your singleton is trivially destructible.

In other words, if std::mutex is trivially destructible, then my code sample above is safe: m is either destructed after bar, or it is "technically destructed" before bar but wouldn't cause a crash anyway. However, if std::mutex is not trivially destructible, then I may need to use the Nifty Counter idiom instead, or alternatively the simpler but "purposefully leaking" Trusty Leaking idiom.

Related:

  • https://stackoverflow.com/a/335746/1951907
  • https://stackoverflow.com/a/17712497/1951907
like image 368
Boris Dalstein Avatar asked Mar 17 '18 22:03

Boris Dalstein


People also ask

Is std :: string trivially destructible?

But apparently std::string is not trivially destructible.

Is std :: mutex fair?

On windows e.g. mutexes are mostly fair, but not always. Some implementations e.g. Thread Building Block provide special mutexes that are fair, but these are not based on the OSes native mutexes, and are usually implemented as spin-locks (which have their own caveats). Save this answer.

Is STD mutex slow?

Taking a std::unique_lock of a std::mutex is significantly slower than taking a std::unique_lock of a std::shared_mutex. This is despite the fact that they both offer the exact same lock constraints, and underneath the hood both of them are just calling RtlAcquireSRWLockExclusive().

Is std :: mutex movable?

std::mutex is neither copyable nor movable.


2 Answers

What the Standard Says

The answer is "no": according to the C++17 Standard, the type std::mutex is not required to have a trivial destructor. General requirements of mutex types are described in [thread.mutex.requirements], and the only paragraph describing destructibility is the following:

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.

Later, the section [thread.mutex.class] details std::mutex in particular but does not specify additional requirements apart from the following paragraph:

The class mutex shall satisfy all of the mutex requirements (33.4.3). It shall be a standard-layout class (Clause 12).

Though, note that among all mutex types, std::mutex is the only one with a constexpr constructor, which is often a hint that the type might be trivially destructible as well.

What Compilers Say

(thanks @liliscent for creating the test)

#include <iostream>
#include <type_traits>
#include <mutex>
using namespace std;
int main()
{
    std::cout << boolalpha << is_trivially_destructible<mutex>::value << "\n";
}
  • Run with Clang 7.0.0: false
  • Run with GCC 8.0.1: true
  • Run with MSVC Version 19.00.23506: false

In other words, it seems that currently, only GCC on Linux platforms provides a trivial destructor for std::mutex.

However, note that there is a Bug Request to make std::mutex trivially destructible in Clang on some platforms:

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.

A follow-up message details that platforms where this change is possible seem to include NPTL (GLIBC) and Apple, while it doesn't seem possible on FreeBSD.

Note that the bug request also mentions the problem I was referring to in my question (emphasis mine):

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".

What Should I Do?

If you need a global mutex in portable code (for example to protect another global object such as a memory pool, etc.), and are in a use case that may be subject to the "static deinitialization order fiasco", then you need to use careful singleton techniques to ensure that the mutex is not only created before first use, but also destructed after last use (or not destructed at all).

The simplest approach is to purposefully "leak" a dynamically allocated local static mutex, like so, which is fast and most likely safe:

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

Otherwise, a cleaner approach is to use the Nifty Counter idiom (or "Schwarz Counter") to control the lifetime of the mutex, although be aware that this technique introduces a small overhead at startup and termination of the program.

like image 58
Boris Dalstein Avatar answered Sep 19 '22 16:09

Boris Dalstein


On VC++ std::mutex is not trivially destructible, so the answer to your question is no.

What (I think) you really want to know is how to be sure the destructor for Bar bar is called before the destructor for foo::m. Well you can't unless they are in the same translation unit. If you define them both in a file called foobar.cpp and define foo() above Bar bar, you're good.

like image 31
Jive Dadson Avatar answered Sep 20 '22 16:09

Jive Dadson