Search code examples
c++multithreadingmutexatomiclock-free

std::atomic for built-in types - non-lock-free vs. trivial destructor?


Looking at std::atomic and it's default specializations I read:

These specializations have standard layout, trivial default constructors, and trivial destructors.

I also read for is_lock_free:

All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. Atomic types are also allowed to be sometimes lock-free, e.g. if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks.

Now here's the catch that I don't get:

How can any atomic type where the Standard prescribes trivial ctor/dtor ever be using any kind of mutex -- all mutexes I ever came across required non-trivial initialization.

This leads to the following questions:

  • Do major platforms provide any locking operation (like a mutex) that is "initalization free" per object. (That would be the "other locking operations".)
  • Is there any known implementation today for default std::atomic specializations that isn't lock free (and still fulfills the trivial ctor/dtor requirement)?
  • Am I simply confusing something here? :-)

It seems to me that even the simplest spin lock (see atomic_flag) needs non-trivial initialization, so I fail to see how this could be implemented.

Disclaimer: Purely out of academic interest, as this kinda jumped out on me while reading these docs.


Solution

  • Here is a possible solution: if an atomic operation uses a lock but has a trivial constructor and destructor, the mutex may be a global mutex shared between many atomic values.

    This, I believe, is the case that the standard authors were allowing. It is possible to use trivial constructors and destructors for mutexes with static duration on some common platforms (such as POSIX).

    // This is copied plain C here, not C++
    // So nothing fancy
    #include <pthread.h>
    pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
    

    If the std::atomic default constructors were permitted to be non-trivial, then it would be difficult to use them during initialization.

    std::atomic<int> my_flag;
    

    Because my_flaghas a trivial constructor, it is static-initialized. Static initialization happens before dynamic initialization. So you can be sure that all the global std::atomic variables are initialized before your constructors run.