reorder
int a = 0;
int b = 0;
// thread A
void foo()
{
a = b + 1;
b = 1;
}
// thread B
void goo()
{
if(b == 1)
{
// a == 1 을 보장할수 있을까?
// 보장할순 없다.)
// 최적화 옵션에 따라 foo함수에서 b = 1이 먼저실행되기도 a = b + 1이 먼저 실행되기도 한다.
}
}
왜 그렇게 될까?
결론부터 말하면 re-ordering때문이다.
void foo()
{
a = b + 1; // a를 쓰기위해서 b을 읽어와야한다.
b = 1; // 어차피 여기서 b에 1을 넣어야 하기에
// a = b + 1;에서 b을 읽어왔을때 b를 먼저 1을 넣어버린다면? -> re-ordering
// 컴파일러 나름대로 성능향상을 볼 수 있을것이다.
// 이것이 문제가 된다.
}
- 성능향상을 위해 코드의 실행순서를 변경하게 되는데
- 컴파일, 실행시간에 발생하며 이로 인해서 생각지 못한 문제가 발생할 수 있다
해결책은 없나
fence
이용
#include <atomic>
// ...
void foo()
{
a = b + 1;
// 위 코드가 아래로 넘어갈수 없다.
std::atomic_thread_fence(std::memory_order_release);
b = 1;
}
memory_order
#include <thread>
int x = 0;
int y = 0;
// foo, goo를 볼때
// 1. 스레드 세이프한가(atomic operation 가능)
// 2. re-ordering으로 코드의 순서가 변경될 일이 없는가
// 를 중점으로 확인해 보자
void foo()
{
// 보면 알겠지만 atomic하지도 re-ordering이 나타지 않을 것이란 보장도 없다.
// 멀티스레드에서는 사용하면 안되는 코드이다.
int n1 = y;
x = n1;
}
void goo()
{
int n2 = x;
y = 100;
}
int main()
{
std::thread t1(foo);
std::thread t2(goo);
t1.join(); t2.join();
}
해결해보자
#include <thread>
#include <atomic>
std::atomic<int> x = 0;
std::atomic<int> y = 0;
void foo()
{
int n1 = y.load(std::memory_order_relaxed);
// std::memory_order_relaxed : atomic만 보장, re-ordering은 보장하지 않음, 단, 오버헤드가 가장 작다
x.store(n1, std::memory_order_relaxed);
}
void goo()
{
int n2 = x.load(std::memory_order_relaxed);
y.store(100, std::memory_order_relaxed);
}
int main()
{
std::thread t1(foo);
std::thread t2(goo);
t1.join(); t2.join();
}
#include <thread>
#include <atomic>
#include <cassert>
std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;
std::atomic<int> flag = 0;
void foo()
{
data1.store(100, std::memory_order_relaxed);
data2.store(200, std::memory_order_relaxed);
flag.store(1, std::memory_order_relaxed);
}
void goo()
{
if(flag.load(std::memory_order_relaxed) > 0)
{
// re-ordering이 보장되지 않기에 assert이 발생할 수 있다.
assert(data1.load(std::memory_order_relaxed) == 100);
assert(data2.load(std::memory_order_relaxed) == 200);
}
}
int main()
{
std::thread t1(foo);
std::thread t2(goo);
t1.join(); t2.join();
}
void foo()
{
data1.store(100, std::memory_order_relaxed);
data2.store(200, std::memory_order_relaxed);
flag.store(1, std::memory_order_release);
// std::memory_order_release 이전의 코드는 std::memory_order_acquire이후에 읽을수 있음을 보장
// 무조건 data1.store(100, std::memory_order_relaxed);, data2.store(200, std::memory_order_relaxed); 실행을 보장
}
void goo()
{
if(flag.load(std::memory_order_acquire) > 0)
{
// re-ordering이 보장되지 않기에 assert이 발생할 수 있다.
assert(data1.load(std::memory_order_relaxed) == 100);
assert(data2.load(std::memory_order_relaxed) == 200);
}
}
#include <thread>
#include <atomic>
#include <cassert>
std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;
int main()
{
// std::memory_order_seq_cst : atomic, re-ordering 모두 보장해 달라
data1.store(100, std::memory_order_seq_cst);
data2.store(200, std::memory_order_seq_cst);
data2.store(300); // 디폴트가 std::memory_order_seq_cst이고 오버헤드가 가장 큼.
}