2 분 소요

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번이 있을 때 다음 그림처럼
Image
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 객체들이 원자적 연산을 할 때 어떤 방식으로 접근할지 옵션으로 결정할 수 있다

  1. memory_order_seq_cst
  2. memory_order_acquire, memory_order_release
  3. 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이 나올 수도 있다

태그: ,

카테고리:

업데이트:

댓글남기기