MPMC에서 적용되는 lock-free buffer queue만들기를 하던 도중, 참고한 코드에서 std::memory_order_relaxed
를 쓰고 있었다.
모두의 코드의 C++ memory order 와 atomic 객체에선 C++의 멀티 스레딩 내에서 스레드 간 실행 순서 보장을 위한 기법으로 소개되고 있다.
std::atomic
먼저 std::atomic
이 어떻게 동작하는지 간략하게 요약해 보면...
스레드 1과 2가 있고, 이 둘이서 std::atomic<int>
로 선언된 변수 a
에 값을 1씩 더한다고 해보자.
만약 a
가 일반적인 int
로 선언되었으면, 한 CPU에서 연산은 다음과 같다.
참고로 여기서 L1, L2 캐시는 CPU 코어 마다 할당돼 있다.
즉, 위 연산을 원자적이지 않게 수행하면 CPU 코어의 캐시마다 저장돼 있는 '현재 변수 a
의 값'이 서로 다를 수 있다.
이를 '캐시 일관성이 없다'라고 말한다.
원자적이게 1~4 연산을 하게 되면,
어셈블리에서 1~4 연산은 한 덩어리로 취급된다.
또한, 어느 한 스레드를 실행하는 CPU 코어가 변수 a
에 대한 연산을 할 동안에는 다른 스레드를 실행하는 CPU 코어들은...
해당 메모리 위치에 대한 메모리 접근이 막히거나 (Bus Lock),
접근하여 캐시에 복사를 했어도 무효화된다 (Cache Lock).
std::memory_order
는 뭔데?다음과 같은 코드가 있다고 할 때,
#include <atomic>
#include <cstdio>
#include <thread>
#include <vector>
using std::memory_order_relaxed;
void t1(std::atomic<int>* a, std::atomic<int>* b)
{
b->store(1, memory_order_relaxed); // b = 1 (쓰기)
int x = a->load(memory_order_relaxed); // x = a (읽기)
printf("x : %d \n", x);
}
void t2(std::atomic<int>* a, std::atomic<int>* b)
{
a->store(1, memory_order_relaxed); // a = 1 (쓰기)
int y = b->load(memory_order_relaxed); // y = b (읽기)
printf("y : %d \n", y);
}
int main()
{
std::vector<std::thread> threads;
std::atomic<int> a(0);
std::atomic<int> b(0);
threads.push_back(std::thread(t1, &a, &b));
threads.push_back(std::thread(t2, &a, &b));
for (int i = 0; i < 2; i++) {
threads[i].join();
}
}
아토믹 연산에 대해 memory_order_relaxed
를 인수로 넣으면
printf(...)
를 호출하는 시점에 a
와 b
는 0
도 나오고 1
도 나온다.
둘다 0
이 나오기도 한다.
즉, 아토믹 연산의 순서를 풀어주는 것.
그러면 엄격하게 코드가 쓰인 순서대로 실행하도록 만드는 방법도 있을 것인데,
그게 바로 memory_order_release
, memory_order_acquire
, memory_order_acq_rel
이다.
memory_order_release
를 인수로 넣으면 해당 아토믹 연산 앞의 코드 보다 먼저 실행될 수 없다.
반대로 memory_order_acquire
를 인수로 넣음녀 해당 아토믹 연산 뒤의 코드보다 후에 실행될 수 없다.
전후 코드 모두에 대해 순서를 보장하고 싶은 경우 memory_order_acq_rel
를 쓴다.
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
void producer(std::atomic<bool>* is_ready, int* data) {
*data = 10;
is_ready->store(true, std::memory_order_release);
}
void consumer(std::atomic<bool>* is_ready, int* data) {
// data 가 준비될 때 까지 기다린다.
while (!is_ready->load(std::memory_order_acquire)) {
}
std::cout << "Data : " << *data << std::endl;
}
int main() {
std::vector<std::thread> threads;
std::atomic<bool> is_ready(false);
int data = 0;
threads.push_back(std::thread(producer, &is_ready, &data));
threads.push_back(std::thread(consumer, &is_ready, &data));
for (int i = 0; i < 2; i++) {
threads[i].join();
}
}