std ::原子。示例中的C ++内存模型

要编写高效且正确的多线程应用程序,了解执行线程之间存在哪些内存同步机制,多线程编程元素(例如互斥锁,联接线程等)提供哪些保证非常重要。 C ++内存模型尤其如此,该模型设计得很复杂,可以为许多处理器体系结构提供最佳的多线程代码。顺便说一句,建立在LLVM上的Rust编程语言使用与C ++中相同的内存模型。因此,本文中的材料对于使用两种语言的程序员都是有用的。但是所有示例都将使用C ++。我将讨论std::atomicstd::memory_order以及在哪三个大象上是原子。


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 ++内存模型和无锁编程。https://www.youtube.com/watch?v=14ntPfyNaKE




All Articles