Thread_11_C++ Atomic 심화
1. 원자성(Atomicity)
어떤 연산이 원자(atomic)적으로 일어난다 는 말의 의미?
CPU에서 명령어 1개로 처리하는 명령, 단 1번의 연산을 의미한다
더 이상 명령을 쪼갤 수 없기 때문에 원자적이라고 함
Cache 가시성과 컴파일러 코드 재배치로 인해서
Thread마다 공유자원을 접근할 때 그 결과가 달라지는 경우가 발생하는데
원자적 연산에서 Thread 사이에 보장되는 법칙이 있다
원자적 연산의 특징
C++에서 원자적인 연산은 모든 Thread들이 ==동일 객체==를 관찰했을 때 ==수정순서(modification order, memory order)==는 동일하다
즉, a라는 변수가 0>2>5>4>8순으로 변했고
Thread1, 2, 3, 4번이 있을 때 다음 그림처럼
thread들은 시간 방향대로 값을 읽어올 수 있다. thread마다 언제 읽어오는지는 모름
하지만 thread4번처럼 과거시점의 값은 읽을 수 없다.
2라는 값을 관측하고 관찰이 안되서 4 시점에서 2일수는 있지만
5번을 관측하고 다시 2번을 관측할 수는 없다
2. Atomic 의 여러 함수
#include <iostream>
#include <atomic>
std::atomic<bool> flag = false;
void main()
{
//현재 cpu에서 atomic의 type이 원자적으로 일어나는지 검사해주는 함수
flag.is_lock_free();
//flag = true; 와 같은 의미
flag.store(true, std::memory_order::memory_order_seq_cst);
//val = flag; 와 같은 의미
bool val = flag.load(std::memory_order::memory_order_seq_cst);
//이전 값을 prev에 넣고 flag를 수정
//bool prev = flag;
//flag = true;
bool prev = flag.exchange(true);
//CAS(Compared-And-Swap)
bool expected = false;
bool desired = true;
flag.compare_exchange_strong(expected, desired);
flag.compare_exchange_weak(expected, desired);
}
is_lock_free의 경우 해당 type이 원자적으로 실행이 가능한지를 검사해주는데
현재 CPU가 해당 type의 원자적 연산 명령을 가지고 있는지 없는지를 검사할 수 있다
3. Atomic의 memory_order
C++ Atomic에서는 atomic 객체들이 원자적 연산을 할 때 어떤 방식으로 접근할지 옵션으로 결정할 수 있다
- memory_order_seq_cst
- memory_order_acquire, memory_order_release
- memory_order_relaxed
이렇게 3가지가 있는데 하나씩 알아보면
1. memory_order_seq_cst(sequential consistency)
atomic 객체의 default 옵션으로서 가장 엄격한 옵션이다
해당 옵션으로 하면 Cache 가시성과 컴파일러 코드 재배치의 문제를 전부 해결할 수 있다
옵션을 주지 않아도 default 로 설정되어있기 때문에 그대로 쓰면 해당 옵션을 사용하게 된다
Cache 가시성과 컴파일러 코드 재배치의 두 문제를 해결한걸 순차적 일관성(sequential consistency)이라고 함
사실상 multi thread에서 가장 문제되는 부분을 해결해주는 것이기 때문에
대부분 해당 옵션을 사용한다
CPU에 따라서 해당 옵션이 과도한 연산을 할 수도 있음
Intel, AMD의 CPU는 순차적 일관성을 보장하는 CPU라서 memory_order_seq_cst옵션을 사용해도 성능차이가 없다 하지만 ARM (핸드폰에 들어가는 CPU) 방식이라면 해당 옵션이 많이 무겁다
2. memory_order_acquire, memory_order_release
atomic 객체의 memory_order 옵션 중 중간 옵션에 해당한다
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<bool> is_ready;
int data;
void producer()
{
data = 10;
is_ready.store(true, std::memory_order_release);
//-----------------절취선----------------------------
//절취선 위에 있던 명령들은 절취선 밑으로 코드 재배치가 될 수 없음
}
void consumer()
{
//절취선 아래에 있던 명령들은 절취선 위로 코드 재배치가 될 수 없음
//-----------------절취선----------------------------
while (!is_ready.load(std::memory_order_acquire))
{ }
std::cout << "Data : " << data << std::endl;
}
int main()
{
is_ready = true;
data = 0;
std::thread t1 = std::thread(producer);
std::thread t2 = std::thread(consumer);
t1.join();
t2.join();
}
release와 acquire 가 보통 같이 다닌다 부분적으로 코드 재배치를 막아주는 옵션
3. memory_order_relaxed
Cache 가시성과 컴파일러 코드 재배치 두 문제를 전부 해결하지 않는 옵션이다
이렇게 되면 race condition에서 매번 말해왔던 문제가 그대로 발생하게 된다
따라서 가장 쓰이지 않는 위험한 옵션이다.
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<bool> is_ready;
int data;
void producer()
{
data = 10;
is_ready.store(true, std::memory_order_relaxed);
}
void consumer()
{
while (!is_ready.load(std::memory_order_relaxed))
{ }
std::cout << "Data : " << data << std::endl;
}
int main()
{
is_ready = true;
data = 0;
std::thread t1 = std::thread(producer);
std::thread t2 = std::thread(consumer);
t1.join();
t2.join();
}
코드 재배치로 인해서 위의 코드의 결과가 data가 0인 상태로 출력될 수 있다
void producer()
{
is_ready.store(true, std::memory_order_relaxed);
data = 10;
}
void consumer()
{
std::cout << "Data : " << data << std::endl;
while (!is_ready.load(std::memory_order_relaxed))
{ }
}
이렇게 코드가 재배치되고 thread 끼리의 순서가 엉키게 되면
data가 0이 나올 수도 있다
댓글남기기