이번 시간에는 C++의 atomic, memory order 등에 관해서 알아보겠습니다.
std::atomic
스레드간 동기화 문제가 발생할 때 해결하는 방법
- OS 제공 동기화 도구 사용 std::mutex
- CPU가 제공하는 OPCODE 사용 (인텔의 경우 lock prefix 사용, atomic operation)
#include <iostream>
#include <thread>
#include <mutex>
#include <windows.h>
std::mutex m;
long x = 0; // 모든 스레드가 공유.
void foo()
{
for ( int i = 0; i < 100000; ++i)
{
// m.lock();
// ++x;
// m.unlock();
// __asm
// {
// lock inc x
// }
InterlockedIncrement(&x);
}
}
int main()
{
std::thread t1(foo);
std::thread t2(foo);
std::thread t3(foo);
t1.join();
t2.join();
t3.join();
std::cout << x << std::endl;
}
C++ 표준에서 제공하는 원자 연산 방법
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<long> x{0};
void foo()
{
for ( int i = 0; i < 100000; ++i)
{
// atomic op flag
++x; // x.operator++()
}
}
int main()
{
std::thread t1(foo);
std::thread t2(foo);
std::thread t3(foo);
t1.join();
t2.join();
t3.join();
std::cout << x << std::endl;
}
std::atomic 연산
구 분 |
내 용 |
연산자 재정의 |
++, –, +=, -=, &=, ^=, |= |
멤버 함수 |
fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor, is_lock_free, load, store |
- fetch_ 함수들은 memory_order를 인자로 전달 할 수 있음
lock-free
- OS 수준 동기화 도구를 사용하지 않고, CPU level의 명령어를 사용해서 동기화 하는 것
memory order
Reordering
- 성능 향상을 위해서 어셈블리 레벨에서 코드의 실행훈서를 변경하는 것
- 컴파일러 최적화에 의해 하단 코드가 의도한대로 동작하지 않을 수 있음(컴파일 시간 or 런타임 모두 발생)
#include <atomic>
int a = 0;
int b = 0;
// thread A
void foo()
{
a = b + 1;
//----- fence -----------
//__asm { mfence }
//std::atomic_thread_fence( std::memory_order_release); // reordering을 막는 방법
b = 1;
}
// thread B
void goo()
{
if ( b == 1 )
{
// a == 1 을 보장할수 있을까 ?
}
}
memory order 옵션
구 분 |
내 용 |
memory_order_relaxed |
오버헤드 가장 적음, atomic operation만 보장, 실행 순서 변경 가능 |
memory_order_consume |
|
memory_order_acquire |
|
memory_order_release |
|
memory_order_acq_rel |
|
memory_order_cst |
|
#include <atomic>
#include <thread>
std::atomic<int> x = 0;
std::atomic<int> y = 0;
void foo()
{
int n1 = y.load(std::memory_order_relaxed);
x.store(n1, std::memory_order_relaxed);
}
void goo()
{
int n2 = x.load(std::memory_order_relaxed);
y.store(100, std::memory_order_relaxed);
}
void main()
{
std::thread t1(foo);
std::thread t2(goo);
}
release-acqure 모델
- release 이전의 코드는 acquire 이후에 읽을 수 있다는 것을 보장
#include <atomic>
#include <thread>
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_release);
}
void goo()
{
if (flag.load(std::memory_order_acquire) > 0)
{
assert(data2.load(std::memory_order_relaxed) == 10);
assert(data2.load(std::memory_order_relaxed) == 20);
}
}
void main()
{
std::thread t1(foo);
std::thread t2(goo);
}