Am I right in the following assumptions:
std::atomic<T>
objects from different threads on any platform with my own synchronization objectsstd::atomic<T>
operations could be lock-free or non-lock-free, dependent on the platformstd::atomic_bool
and std::atomic<bool>
(and the other types like these) are the same things actuallystd::atomic_flag
is the only class that guarantees platform-independent lock-free operations by the standardAlso where can I find a useful info about std::memory_order
and how to use it properly?
Yes, it would be threadsafe. Assuming of course there are no bugs in the std::atomic implementation - but it's not usually hard to get right. This is exactly what std::atomic is meant to do.
(C++11) [edit] The atomic library provides components for fine-grained atomic operations allowing for lockless concurrent programming. Each atomic operation is indivisible with regards to any other atomic operation that involves the same object. Atomic objects are free of data races.
In order to solve this problem, C++ offers atomic variables that are thread-safe. The atomic type is implemented using mutex locks. If one thread acquires the mutex lock, then no other thread can acquire it until it is released by that particular thread.
std::atomic compiles to lock addq . The LOCK prefix makes the following inc fetch, modify and update memory atomically. our explicit inline assembly LOCK prefix compiles to almost the same thing as std::atomic , except that our inc is used instead of add .
Let's go through one by one.
std::atomic<T>
objects from different threads on any platform with my own synchronization objectsYes, atomic
objects are fully synchronized on all of their accessor methods.
The only time a data race can occur with access to the atomic types is during construction, but it involves constructing an atomic object A
, passing its address to another thread via an atomic pointer using memory_order_relaxed
to intentionally work around the sequential consistency of std::atomic
, and then accessing A
from that second thread. So, don't do that? :)
Speaking of construction, there are three ways to initialize your atomic types:
// Method 1: constructor
std::atomic<int> my_int(5);
// Method 2: atomic_init
std::atomic<int> my_int; // must be default constructed
std::atomic_init(&my_int, 5); // only allowed once
// Method 3: ATOMIC_VAR_INIT
// may be implemented using locks even if std::atomic<int> is lock-free
std::atomic<int> my_int = ATOMIC_VAR_INIT(5);
Using either of the two latter methods, the same data race possibility applies.
std::atomic<T>
operations could be lock-free or non-lock-free, dependent on the platformCorrect. For all integral types, there are macros you can check that tell you whether a given atomic
specialization is not, sometimes, or always lock-free. The value of the macros is 0, 1, or 2 respectively for these three cases. The full list of macros is taken from §29.4 of the standard, where unspecified
is their stand-in for "0, 1, or 2":
#define ATOMIC_BOOL_LOCK_FREE unspecified
#define ATOMIC_CHAR_LOCK_FREE unspecified
#define ATOMIC_CHAR16_T_LOCK_FREE unspecified
#define ATOMIC_CHAR32_T_LOCK_FREE unspecified
#define ATOMIC_WCHAR_T_LOCK_FREE unspecified
#define ATOMIC_SHORT_LOCK_FREE unspecified
#define ATOMIC_INT_LOCK_FREE unspecified
#define ATOMIC_LONG_LOCK_FREE unspecified
#define ATOMIC_LLONG_LOCK_FREE unspecified
#define ATOMIC_POINTER_LOCK_FREE unspecified
Note that these defines apply to both the unsigned and signed variants of the corresponding types.
In the case that the #define
is 1, you have to check at runtime. This is accomplished as follows:
std::atomic<int> my_int;
if (my_int.is_lock_free()) {
// do lock-free stuff
}
if (std::atomic_is_lock_free(&my_int)) {
// also do lock-free stuff
}
std::atomic_bool
and std::atomic<bool>
(and the other types like these) are the same things actuallyYes, these are just typedef
s for your convenience. The full list is found in Table 194 of the standard:
Named type | Integral argument type
----------------+-----------------------
atomic_char | char
atomic_schar | signed char
atomic_uchar | unsigned char
atomic_short | short
atomic_ushort | unsigned short
atomic_int | int
atomic_uint | unsigned int
atomic_long | long
atomic_ulong | unsigned long
atomic_llong | long long
atomic_ullong | unsigned long long
atomic_char16_t | char16_t
atomic_char32_t | char32_t
atomic_wchar_t | wchar_t
std::atomic_flag
is the only class that guarantees platform-independent lock-free operations by the standardCorrect, as guaranteed by §29.7/2 in the standard.
Note that there's no guarantee on the initialization state of atomic_flag
unless you initialize it with the macro as follows:
std::atomic_flag guard = ATOMIC_FLAG_INIT; // guaranteed to be initialized cleared
There is a similar macro for the other atomic types,
The standard does not specify if atomic_flag
could experience the same data race other atomic types could have during construction.
std::memory_order
and how to use it properly?As suggested by @WhozCraig, cppreference.com has the best reference.
And as @erenon suggests, Boost.Atomic has a great essay on how to use memory fences for lock-free programming.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With