std :: atomic. C ++ memory model in examples

To write efficient and correct multithreaded applications, it is very important to know what mechanisms of memory synchronization exist between threads of execution, what guarantees are provided by elements of multithreaded programming, such as a mutex, join threads, and others. This is especially true of the C ++ memory model, which was designed to be complex to provide optimal multi-threaded code for a variety of processor architectures. By the way, the Rust programming language, being built on LLVM, uses the same memory model as in C ++. Therefore, the material in this article will be useful for programmers in both languages. But all examples will be in C ++. I will talk about std::atomic, std::memory_orderand on which three elephants are the atoms.


C++11 C++, . . , . . , , . - ( ). , , : , . . , , . - . x86-64 ARM , .

C++ , ++11 , , .

: C++ — "" , . C++ , undefined behavior (UB), , .

, C++, , . , .

, . (std::atomic), .. "" . , (std::mutex) , , .  , .

, C++ , . ?

  1. … .

  2. .

  3. .

— , , . . 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.

, :

  1. , ,

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

std::thread::(constructor) vs

std::thread (release) (acquire). , .

std::thread::join vs

join , join, "" , .

std::mutex::lock vs std::mutex::unlock

lock , unlock.

std::promise::set_value vs std::future::wait

set_value wait.

. [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. C ++ Memory Model & Lock-Free Programming. https://www.youtube.com/watch?v=14ntPfyNaKE




All Articles