이번엔 thread_fence에 대해서 정리하려 해.
앞서 operation with release/acquire/consume semantic에 대해 정리했는데, 이것들은 모두 어떤 특정 operation(LOAD나 STORE)에 대해 오더링을 지정하는거였어.
thread fence는 특정 operation 없이 오더링을 지정하는 역할을 해.
전에 썼던 acquire/release operation을 다시 보자. acquire load 아래쪽의 load나 store는 위쪽으로 못올라오고 release store 위쪽의 load나 store는 아래쪽으로 못내려온다고 했었어.
LOAD, acquire // 아래쪽의 LOAD나 STORE가 위로 못올라옴
LOAD
STORE
... // 얘네들 끼리는 리오더링이 가능함.
LOAD
STORE
STORE, release // 위쪽의 LOAD나 STORE가 아래로 못내려옴.
그런데, acquire load 위쪽, 또는 release store 아래쪽의 load나 store는 어떨까?
다음 상황을 보자. OP는 load나 store등의 operation을 나타내.
OP A
LOAD B, acquire
OP C
STORE D, release
OP E
여기서 OP A는 acquire아래로 내려갈 수 있지만 release 밑으로는 못내려가겠지? OP E는 release 위로는 가지만 acquire 위로는 못가게 될거야.
그러니까 다음과 같은 순서로 진행될 수도 있어.
LOAD B, acquire
OP E
OP C
OP A
STORE D, release
그럼 thread fence는 어떻게 다른가 보자. thread fence는 쉽게 말하자면 다음과 같아. 비교를 위해 acquire operation도 같이 정리했어.
acquire operation : 뒤의 모든 operation은 (해당 LOAD ) 이후에 일어난다.
acquire fence : 뒤의 모든 operation은 (해당 fence 앞의 모든 LOAD) 이후에 일어난다.
release operation : 앞선 모든 operation은 (해당 STORE ) 이전에 일어난다.
release fence : 앞선 모든 operation은 (해당 fence 뒤의 모든 STORE) 이전에 일어난다.
preshing의 한 포스트(http://preshing.com/20131125/acquire-and-release-fences-dont-work-the-way-youd-expect/) 에서도 지적하듯이 fence에 대해 잘못 이해하는 경우가 많은데(심지어 herb sutter조차도 실수를 했다 그러네), release operation과 release fence, acquire operation과 acquire fence는 비슷하지만 다르기 때문에 정확하게 이해해야해.
release fence의 간단한 예를 한번 release operation과 비교해서 보자.
LOAD A // B 밑으로 못내려감
STORE B, release
STORE C // B 위로 올라갈 수 있음
위 예는 다음과 같은 순서로도 진행될 수 있겠지?
STORE C
LOAD A
STORE B, release
그런데, release fence를 쓰면 달라.
LOAD A // release fence 뒤의 모든 store보다 먼저 일어남
FENCE, release
STORE B // 따라서 A는 B와 C보다 항상 먼저 일어나야함
STORE C
여기서 가능한 진행 상황은 딱 두가지야. B가 C보다 먼저 일어나거나, C가 B보다 먼저 일어나거나. B나 C가 A 이전에 일어날 일은 절대 없어.
singleton에서의 예도 같이 한번 보자. (위에서 언급한 preshing의 포스트에서 가져옴)
std::atomic<Singleton*> m_instance;
Singleton* tmp = new Singleton;
m_instance.store(tmp, std::memory_order_release);
release fence를 사용하면 다음과 같이 쓸 수 있어.
Singleton* m_instance;
Singleton* tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
m_instance = tmp;
그런데 만약에, 누군가 fence와 operation을 잘못 이해해서 release fence를 다음과 같이 release operation으로 대체했다고 생각해보자.
Singleton* m_instance;
std::atomice<int> g_dummy;
Singleton* tmp = new Singleton;
g_dummy.store(0, std::memory_order_release);
m_instance = tmp;
release operation의 역할은 해당 operation 앞의 모든 load, store가 먼저 일어나도록 하는거였지? 그럼 맨 마지막 줄 m_instance = tmp;는 g_dummy 위로 올라갈 수 있을테고, 다음과 같이 실행될 수도 있어.
Singleton* tmp = new Singleton;
m_instance = tmp; // BOOM!!
g_dummy.store(0, std::memory_order_release);
thread safe한 singleton을 만드는게 목표였는데 thread safe하지 않게 됨을 알 수 있지.
다음에는 Read-Modify-Write(RMW) 에 대해서 정리하고 이를 이용한 간단한 spinlock 구현에 대해 정리해볼게.
모두 좋은 하루!
선개념누르고 잇다읽어보겟습니다 - DCW