
여기서는 Pipeline의 효율 최대화를 위한 코드 재배치에 대해 알아볼 것이며, Pipeline Hazard, RISC, CISC에 관한 내용은 생략합니다.
#include <iostream>
#include <atomic>
#include <mutex>
#include <windows.h>
#include <future>
using namespace std;
int32 x = 0;
int32 y = 0;
int32 r1 = 0;
int32 r2 = 0;
volatile bool ready;
void Thread_1()
{
while(!ready);
y = 1; // Store y
r1 = x; // Load x
}
void Thread_2()
{
while(!ready);
x = 1; // Store x
r2 = y; // Load y
}
int main()
{
int32 count = 0;
while(true)
{
ready = false;
count++;
x = y = r1 = r2 = 0;
thread t1(Thread_1);
thread t2(Thread_2);
ready = true;
t1.join();
t2.join();
if(r1 == 0 && r2 == 0)
break;
}
// Break로 빠져나온 경우
cout << cout << " 번만에 빠져 나옴" << endl;
return 0;
}
r1 == 0 && r2 == 0인 상황이 로직상 발생하면 안된다.cache miss 발생시, 데이터를 가져와 캐시에 넣고 쓸 수도 있고, 그냥 바로 메모리 값만 가져올 수도 있다.y = 1; // Store y
r1 = x; // Load x
위 코드를 제멋대로 아래 코드로 바꿀 수 있다.
r1 = x; // Load x
y = 1; // Store y
🤔 무엇을 믿어야 할까?
atomic 연산에 한해, 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 관찰
atomic<typename T> 클래스만을 의미하는 것이 아니라, CPU Instruction이 한 개처럼(또는 한 개인) 동작하는 연산을 의미함.{
atomic<int64> v;
cout << v.is_lock_free() << endl; // 1
}
{
struct Knight
{
int32 level;
int32 hp;
int32 mp;
};
atomic<Knight> v;
cout << v.is_lock_free() << endl; // 0
}
💡 Lock Free = 락에서 자유롭다 = 락 안써도 돼~
is_lock_free() 가 true라면, 사용중인 칩에서 해당 데이터 타입에 대한 원자적 연산을 지원함을 의미함.atomic<bool> flag = false;
int main()
{
// flag = true; 문제가 생기진 않음
// flag.store(true);
flag.store(true, memory_order::memory_order_seq_cst);
// bool val = flag;
// bool val = flag.load();
bool val = flag.load(memory_order::memory_order_seq_cst);
// 이전 flag 값을 prev에 넣고, flag 값을 수정
{
/*
아래 코드는 비 원자적이기 때문에 다른 쓰레드에 의해 의도치 않은 상황 발생할 수 있음
bool prev = flag;
flag = true;
*/
bool prev = flag.exchange(true);
}
// CAS (Compare-And-Swap) 조건부 수정
{
bool expected = false;
bool desired = true;
flag.compare_exchange_strong(expected, desired);
}
}
flag.store(true) 로 쓰면 디폴트 정책인 flag.store(true, memory_order::memory_order_seq_cst)이 선택된다.atomic_thread_fence()같은 것도 있지만 잘 사용하진 않는다.