Para escribir aplicaciones multiproceso eficientes y correctas, es muy importante saber qué mecanismos de sincronización de memoria existen entre los hilos de ejecución, qué garantías brindan los elementos de programación multiproceso, como un mutex, unir hilos, entre otros. Esto es especialmente cierto en el modelo de memoria C ++, que fue diseñado para ser complejo para proporcionar un código óptimo de múltiples subprocesos para una variedad de arquitecturas de procesador. Por cierto, el lenguaje de programación Rust, construido en LLVM, usa el mismo modelo de memoria que en C ++. Por lo tanto, el material de este artículo será útil para programadores en ambos lenguajes. Pero todos los ejemplos estarán en C ++. Hablaré sobre std::atomic
, std::memory_order
y sobre qué tres elefantes son los átomos.
C++11 C++, . . , . . , , . - ( ). , , : , . . , , . - . x86-64 ARM , .
C++ , ++11 , , .
: C++ — "" , . C++ , undefined behavior (UB), , .
, C++, , . , .
, . (std::atomic
), .. "" . , (std::mutex
) , , . , .
, C++ , . ?
… .
.
.
— , , . . std::atomic, : load
, store
, fetch_add
, compare_exchange_*
. — read-modify-write , .
read-modify-write , . 0, link:
static int v1 = 0;
static std::atomic<int> v2{ 0 };
int add_v1() {
return ++v1;
/* Generated x86-64 assembly:
mov eax, DWORD PTR v1[rip]
add eax, 1
mov DWORD PTR v1[rip], eax
*/
}
int add_v2() {
return v2.fetch_add(1);
/* Generated x86-64 assembly:
mov eax, 1
lock xadd DWORD PTR _ZL2v2[rip], eax
*/
}
v1
int : read-modify-write. , v1
. v2
lock , , , v2
, , .
. , , . . . , , . .
. , , . , , , , . UB.
, :
, ,
, . C++ . : relaxed
, release/acquire
sequential consistency
. .
,
— relaxed
. , . :
""
thread2
"" ,thread1
thread1
thread2
relaxed
. 1, link:
std::atomic<size_t> counter{ 0 };
// process can be called from different threads
void process(Request req) {
counter.fetch_add(1, std::memory_order_relaxed);
// ...
}
void print_metrics() {
std::cout << "Number of requests = " << counter.load(std::memory_order_relaxed) << "\n";
// ...
}
. 2, link:
std::atomic<bool> stopped{ false };
void thread1() {
while (!stopped.load(std::memory_order_relaxed)) {
// ...
}
}
void stop_thread1() {
stopped.store(true, std::memory_order_relaxed);
}
thread1
, stop_thread1
. , thread1
() stopped
true
.
relaxed
. 3, link:
std::string data;
std::atomic<bool> ready{ false };
void thread1() {
data = "very important bytes";
ready.store(true, std::memory_order_relaxed);
}
void thread2() {
while (!ready.load(std::memory_order_relaxed));
std::cout << "data is ready: " << data << "\n"; // potentially memory corruption is here
}
, thread2
data
, ready
, .. relaxed
.
" " (sequential consistency, seq_cst
) . :
thread1
thread2
.
( )
thread1
,store
,load
thread2
seq_cst
, , .
C++ , .. . seq_cst
, . , x86-64 seq_cst
, ARM .
. 4, [1], link:
std::atomic<bool> x, y;
std::atomic<int> z;
void thread_write_x() {
x.store(true, std::memory_order_seq_cst);
}
void thread_write_y() {
y.store(true, std::memory_order_seq_cst);
}
void thread_read_x_then_y() {
while (!x.load(std::memory_order_seq_cst));
if (y.load(std::memory_order_seq_cst)) {
++z;
}
}
void thread_read_y_then_x() {
while (!y.load(std::memory_order_seq_cst));
if (x.load(std::memory_order_seq_cst)) {
++z;
}
}
, , z
1
2
, thread_read_x_then_y
thread_read_y_then_x
"" x
y
. : x = true
, y = true
, y = true
, x = true
.
seq_cst
relaxed
acquire/release
, . seq_cst
, : seq_cst
. 1 2 , relaxed
seq_cst
, 3 .
. Acquire/Release
acquire/release
. : memory_order_acquire
memory_order_release
. :
release
,acquire
thread1
,release
,acquire
thread2
release
thread1
,acquire
thread2
, , . , 4 store
memory_order_release
, load
memory_order_acquire
, z
0, 1 2. , , store
x
y
, thread_read_x_then_y
thread_read_y_then_x
. , load
store
3. , .. ( seq_cst
), .
release
, , . acquire
, "" , . release
acquire
, UB .
, , lock
. spinlock
. , , . 5, link:
class mutex {
public:
void lock() {
bool expected = false;
while(!_locked.compare_exchange_weak(expected, true, std::memory_order_acquire)) {
expected = false;
}
}
void unlock() {
_locked.store(false, std::memory_order_release);
}
private:
std::atomic<bool> _locked;
};
lock()
false true acquire
. compare_exchage_weak
strong
, cppreference. unlock()
false release
. , , . , unlock()
, lock()
. . , .
, Double Checked Locking Anti-Pattern [2]. 6, link:
struct Singleton {
// ...
};
static Singleton* singleton = nullptr;
static std::mutex mtx;
static bool initialized = false;
void lazy_init() {
if (initialized) // early return to avoid touching mutex every call
return;
std::unique_lock l(mtx); // `mutex` locks here (acquire memory)
if (!initialized) {
singleton = new Singleton();
initialized = true;
}
// `mutex` unlocks here (release memory)
}
: Singleton
. , . .. , singleton
read-only , if (initialized) return
. , x86-64. C++. :
void thread1() {
lazy_init();
singleton->do_job();
}
void thread2() {
lazy_init();
singleton->do_job();
}
:
1. thread1
-> :
lock (
acquire
)singleton = ..
initialized = true
unlock (
release
)
2. thread2
:
if(initalized)
true
(,initialized
)singleton->do_job()
segmentation fault
(singleton
thread1
)
, , .
acquire/release
acquire/release
, . .
| |
| |
| lock , unlock. |
|
|
. [1].
? : , std::promise::set_value
std::future::wait
, , , , , set_value
. , -, . , , , , , .
C++ , . , . , , C++. volatile bool
, , , read-modify-write , . , . , !
[1] Anthony Williams. C++ Concurrency in Action. https://www.amazon.com/C-Concurrency-Action-Practical-Multithreading/dp/1933988770
[2] Tony van Eerd. Modelo de memoria C ++ y programación sin bloqueo. https://www.youtube.com/watch?v=14ntPfyNaKE