logo

English

이곳의 프로그래밍관련 정보와 소스는 마음대로 활용하셔도 좋습니다. 다만 쓰시기 전에 통보 정도는 해주시는 것이 예의 일것 같습니다. 질문이나 오류 수정은 siseong@gmail.com 으로 주세요. 감사합니다.

C++ Atomic 클래스에 대해서

by 엉뚱도마뱀 posted Dec 13, 2017
?

Shortcut

PrevPrev Article

NextNext Article

Larger Font Smaller Font Up Down Go comment Print Attachment
?

Shortcut

PrevPrev Article

NextNext Article

Larger Font Smaller Font Up Down Go comment Print Attachment
1. std::atomic
 
atomic 클래스는 정수형 또는 포인터 타입에 대해 산술 연산들을 atomic하게 수행할 수 있도록 해 주는 템플릿 클래스이다.
(더하고 빼고, 그리고 and/or/xor 등의 비트 연산들...)
 
이전에는 atomic 연산을 하기 위해서는 volatile 변수와 interlocked 계열 함수를 일일히 사용해 주어야 했지만, std::atomic 클래스로 인해 그러한 번거로움을 피할 수 있게 된다.
 
그리고, boost 라이브러리나 intel tbb를 통해서만 사용이 가능했던 atomic 클래스가 C++11 표준으로 들어오면서 VS2012부터 사용이 가능해졌다.
 
우선, std::atomic 클래스의 형태는 아래와 같다.
 
  1. template<typename T>
  2. struct atomic;
  3.  
  4. // 각종 정수형에 대한 완전 특수화
  5. template<>
  6. struct atomic<Integral>;
  7.  
  8. // 포인터 타입에 대한 부분 특수화
  9. template<typename T>
  10. struct atomic<T*>;
 
그리고, 정수형에 대해선 다음과 같은 typedef 들을 제공하고 있다.
 
  1. typedef atomic<int8_t> atomic_int8_t;
  2. typedef atomic<uint8_t> atomic_uint8_t;
  3. typedef atomic<int16_t> atomic_int16_t;
  4. typedef atomic<uint16_t> atomic_uint16_t;
  5. typedef atomic<int32_t> atomic_int32_t;
  6. typedef atomic<uint32_t> atomic_uint32_t;
  7. typedef atomic<int64_t> atomic_int64_t;
  8. typedef atomic<uint64_t> atomic_uint64_t;
  9.  
  10. typedef atomic<int_least8_t> atomic_int_least8_t;
  11. typedef atomic<uint_least8_t> atomic_uint_least8_t;
  12. typedef atomic<int_least16_t> atomic_int_least16_t;
  13. typedef atomic<uint_least16_t> atomic_uint_least16_t;
  14. typedef atomic<int_least32_t> atomic_int_least32_t;
  15. typedef atomic<uint_least32_t> atomic_uint_least32_t;
  16. typedef atomic<int_least64_t> atomic_int_least64_t;
  17. typedef atomic<uint_least64_t> atomic_uint_least64_t;
  18.  
  19. typedef atomic<int_fast8_t> atomic_int_fast8_t;
  20. typedef atomic<uint_fast8_t> atomic_uint_fast8_t;
  21. typedef atomic<int_fast16_t> atomic_int_fast16_t;
  22. typedef atomic<uint_fast16_t> atomic_uint_fast16_t;
  23. typedef atomic<int_fast32_t> atomic_int_fast32_t;
  24. typedef atomic<uint_fast32_t> atomic_uint_fast32_t;
  25. typedef atomic<int_fast64_t> atomic_int_fast64_t;
  26. typedef atomic<uint_fast64_t> atomic_uint_fast64_t;
  27.  
  28. typedef atomic<intptr_t> atomic_intptr_t;
  29. typedef atomic<uintptr_t> atomic_uintptr_t;
  30. typedef atomic<size_t> atomic_size_t;
  31. typedef atomic<ptrdiff_t> atomic_ptrdiff_t;
  32. typedef atomic<intmax_t> atomic_intmax_t;
  33. typedef atomic<uintmax_t> atomic_uintmax_t;
 
개인적으로는 그냥 atomic<type>을 더 선호하는 편이라, 위의 typedef 들은 사용하지 않을 듯 하다.
 
그럼 기본적인 기능들에 대해 예제를 살펴보기로 하자.
 
  1. #include <atomic>
  2.  
  3. using namespace std;
  4.  
  5. int _tmain(int argc, _TCHAR* argv[])
  6. {
  7.     ////////////////////////////////////////////////////////////////////////////////
  8.     /// 생성
  9.     ////////////////////////////////////////////////////////////////////////////////
  10.     // 먼저 atomic 객체를 선언하고, 값을 대입 OK
  11.     atomic<int> intAtomic;
  12.     intAtomic = 2;
  13.  
  14.     // atomic 객체간 복사생성/대입연산은 금지되어 있다.
  15.  
  16.     // 복사생성 시도 -> 컴파일 에러
  17.     atomic<int> intAtomic2 = intAtomic;
  18.  
  19.     // 대입연산 시도 -> 컴파일 에러
  20.     atomic<int> intAtomic2;
  21.     intAtomic2 = intAtomic;
  22.  
  23.     // atomic<int>와 atomic<short>는 엄연히 다른 클래스
  24.     // 그렇기에 아래 문장은 다음과 같이 수행된다.
  25.     // 1) int tempInt = intAtomic.load();
  26.     // 2) atomic<short> shortAtomic = tempInt;
  27.     // 따라서 컴파일에 아무런 문제가 없다.
  28.     atomic<short> shortAtomic = intAtomic;
  29.  
  30.     ////////////////////////////////////////////////////////////////////////////////
  31.     /// +,- 연산/논리 연산
  32.     ////////////////////////////////////////////////////////////////////////////////
  33.     // 값 더하기 : 멤버 함수 fetch_add
  34.     intAtomic.fetch_add(1);
  35.     // 값 더하기 : 멤버 함수 operator++(), operator++(int)
  36.     intAtomic++;
  37.     // 값 더하기 : 일반 함수 atomic_fetch_add
  38.     atomic_fetch_add(&intAtomic, 1);
  39.  
  40.     // 값 빼기 : 멤버 함수 fetch_sub
  41.     intAtomic.fetch_sub(1);
  42.     // 값 빼기 : 멤버 함수 operator--(), operator--(int)
  43.     intAtomic--;
  44.     // 값 빼기 : 일반 함수 atomic_fetch_sub
  45.     atomic_fetch_sub(&intAtomic, 1);
  46.  
  47.     // and : 멤버 함수 fetch_and (0x10 & 0x10)
  48.     intAtomic.fetch_and(2);
  49.     // and : 멤버 함수 operator &= (0x10 & 0x10)
  50.     intAtomic &= 2;
  51.     // and : 일반 함수 atomic_fetch_add (0x10 & 0x01)
  52.     atomic_fetch_add(&intAtomic, 1);
  53.  
  54.     // or : 멤버 함수 fetch_or (0x00 | 0x01)
  55.     intAtomic.fetch_or(1);
  56.     // or : 멤버 함수 operator |= (0x01 | 0x10)
  57.     intAtomic |= 2;
  58.     // or : 일반 함수 atomic_fetch_or (0x11 | 0x01)
  59.     atomic_fetch_or(&intAtomic, 1);
  60.  
  61.     // xor : 멤버 함수 fetch_xor(0x11 ^ 0x01)
  62.     intAtomic.fetch_xor(1);
  63.     // xor : 멤버 함수 operator ^= (0x10 ^ 0x01)
  64.     intAtomic ^= 1;
  65.     // xor : 일반 함수 atomic_fetch_xor (0x11 ^ 0x01)
  66.     atomic_fetch_xor(&intAtomic, 1);
  67.  
  68.     ////////////////////////////////////////////////////////////////////////////////
  69.     /// 읽기/저장/교환/비교 교환
  70.     ////////////////////////////////////////////////////////////////////////////////
  71.     // 값 로드 : 멤버 함수 load
  72.     int value = intAtomic.load();
  73.     // 값 로드 : 멤버 함수 Operator T
  74.     value = intAtomic;
  75.     // 값 로드 : 일반 함수 atomic_load
  76.     value = atomic_load(&intAtomic);
  77.  
  78.     // 값 저장 : 멤버 함수 store
  79.     intAtomic.store(3);
  80.     // 값 저장 : 멤버 함수 operator= (T value)
  81.     intAtomic = 3;
  82.     // 값 저장 : 일반 함수 atomic_store
  83.     atomic_store(&intAtomic, 3);
  84.  
  85.     // 값 교환 : 멤버 함수 exchange
  86.     int oldValue = intAtomic.exchange(5);
  87.     // 값 교환 : 일반 함수 atomic_exchange
  88.     oldValue = atomic_exchange(&intAtomic, 3);
  89.  
  90.     int comparand = 5;
  91.     int newValue = 10;
  92.  
  93.     // 값 비교 교환 : 멤버 함수 (value = 3, 5와 같다면, 10으로 value를 바꾸어라)
  94.     // 수행 후 comparand는 원래 value인 3로 바뀐다.
  95.     bool exchanged = intAtomic.compare_exchange_weak(comparand, newValue);
  96.     // 값 비교 교환 : 일반 함수
  97.     // 앞서 comparand가 3로 바뀌었기에, 값이 10으로 바뀐다
  98.     exchanged = atomic_compare_exchange_weak(&intAtomic, &comparand, newValue);
  99.  
  100.     // VS2013에서는 compare_exchange_weak와 compare_exchange_strong 구현이 동일하다.
  101.     // compare_exchange_weak가 compare_exchange_strong을 호출한다.
  102.  
  103.     ////////////////////////////////////////////////////////////////////////////////
  104.     /// 유틸 함수
  105.     ////////////////////////////////////////////////////////////////////////////////
  106.     // std::atomic은 해당 오브젝트 크기가 8바이트 이하이면 atomic을 보장한다.
  107.     // _ATOMIC_MAXBYTES_LOCK_FREE = 8
  108.     bool is_lock_free = intAtomic.is_lock_free();
  109.  
  110.     return 0;
  111. }
 
위 예제에 보면, compare_exchange_weak와 compare_exchange_strong이 나온다.
원래 하나였던 compare_exchange를 둘로 쪼개어 놓은 건데, VS2013에서는 weak 버전이 strong 버전을 호출해 버린다.
 
즉, 아직은 동일하게 구현이 되어 있는 셈인데, 각종 문서를 읽어봐도 둘의 차이를 명확하게 이해하지 못하겠다.
 
혹시 몰라 관련된 링크를 남겨 놓으니, 추후에라도 이해가 되기를...
 
 
2. memory order
 
우선, 메모리 가시성과 장벽에 대한 이해가 수반되면 훨씬 좋다.
 
memory order는 6가지 모델이 존재하며, 자세한 내용은 아래 링크들 참조하자. (내용, 예제 모두 good)
 
참고로, atomic 클래스의 모든 기능들은 인자로 memory_order를 받을 수 있으며, 
특별히 memory_order를 지정하지 않은 채 함수들을 호출하면 기본적으로 memory_order_seq_cst가 적용된다.
(즉, sequential consistency를 보장한다)
 
따라서, atomic의 메써드들을 특별히 memory_order를 지정하지 않더라도, 모든 쓰레드에서의 memory visibility가 보장된다. 
추가로, memory_order_seq_cst는 memory re-ordering까지 막아준다.
 
정리하면, 멀티 쓰레드 환경에서 memory_order_seq_cst 이전의 코드는 반드시 memory_order_seq_cst 이후의 코드에 보이게 된다는 뜻이다.
 
하지만, 자신의 개발환경이 sequential consistency를 보장하지 않을 수 있고,
상황에 따라 sequential consistency가 불필요한 overhead를 가질 수 있다.
 
예를 들어, producer-consumer만 존재한다면, Write-Release / Read-Acquire로 충분하다.
 
Write-Release / Read-Acquire란, 쓰고 나서 배포하고, 읽기 전에 획득하는 것.
즉, 배포(release)하기 전에 쓴 것(write)들이 획득(acquire)한 이후 읽기(read) 과정에서 보이는 보장을 의미한다.
 
따라서, 상황에 맞게 memory_order를 지정한다면, 최적화된 메모리 가시성 보장을 이루어 낼 수 있다.
 
사실 일반적인 게임 서버 애플리케이션 제작에서는 기본적인 C++ atomic 기능만 사용해도 충분할 듯 하다.
소소한 코드 레벨에서의 최적화보다 쓰레드 모델링 - 컨텐츠 구현의 뼈대가 사실 더 중요한 것이다.
 
그럼에도~ 뭐 알아서 남 주겠는가?
 
Write-Release / Read-Acquire 메커니즘에 대한 예제를 하나 살펴보자.
 
  1. struct Payload
  2. {
  3.    // ...
  4. };
  5.  
  6. // 전역 변수
  7. Payload g_payload;
  8.  
  9. // Memory order 가드용 atomic 변수
  10. atomic<int> g_guard;
  11.  
  12. // 쓰레드 1에서의 producer
  13. void TrySendMessage()
  14. {
  15.     //...
  16.    
  17.     // 전역 변수에 값 쓰기
  18.     g_payload.tick = clock();
  19.     g_payload.str = "TestMessage";
  20.     g_payload.param = param;
  21.  
  22.     // 여기에서 Write-Release 수행
  23.     // 지금까지 쓴 내용들이 Acquire 이후에 보여진다.
  24.     g_guard.store(1memory_order_release);
  25. }
  26.  
  27. // 쓰레드 2에서 대기중인 consumer
  28. void TryReceiveMessage()
  29. {
  30.     // ...
  31.  
  32.     // Load-Acquire 수행
  33.     int ready = g_guard.load(memory_order_acquire);
  34.     // 이후부터는 Release 이전에 쓰여진 메모리 값들이 모두 제대로 보인다
  35.    
  36.     if (ready != 0)
  37.     {
  38.         result.tick = g_payload.tick;
  39.         result.str = g_payload.str;
  40.         result.param = g_payload.param;
  41.     }
  42. }
 
위 코드의 흐름을 그림으로 표현하면 아래와 같이 된다.
 
atomic001.png

 

 
이 때 주의사항이 하나 있는데, Producer가 먼저 호출되고 Consumer가 호출되어야지,
만약 Consumer가 먼저 호출되어 버리면, Write-Release가 수행되지 않았기에 가시성 보장이 깨지게 된다.
 
즉, Thread1에서 새로 바꾼 값들이 아닌 이전의 값을 읽을 수 있다는 것이다.
 
atomic002.png

 

즉, Read-Acquire는 Write-Release가 수행된 이후 Write-Release 이전의 변경 사항들을 제대로 읽을 수 있는 것이다.
 
실무에서는 위와 같이 g_payload의 데이터를 읽고 쓰는 일은 거의 없다고 보면 된다.
(Produce-Consume 호출 순서가 명확히 보장되지 않는 이상...)
 
위 예제는 단순히 Write-Release / Read-Acquire 관계를 설명하기 위한 것이라고 생각하면 좋을 듯 하다.
 
참고로, Intel TBB atomic의 기본 store/load 역시 memory_semantic이 release/acquire로 되어 있다.
 
 
3. fence
 
C++11은 두 개의 memory fence(memory barrier) 기능을 제공한다.
 
fence는 non-atomic operation 또는 memory_order_relaxed로 수행되는 atomic operation시 메모리 배리어 역할을 수행한다.
 
 
1) atomic_thread_fence(memory_order order)
 
메모리 가시성 강제와 메모리 재배치를 금지한다.
사실 fence는 atomic 클래스나 atomic instrinsic 함수들을 사용한다면, 굳이 사용할 필요가 없다고 생각한다.
 
굳이 이를 사용하려면, atomic 멤버 함수 호출시 memory_order_relaxed(메모리 배치에 관여하지 않음) 방식을 쓴 뒤 뒤,
인위적으로 fence를 호출해 주는 방식을 써야 하는데 굳이 쓸 일이 있을까 싶다.
 
앞서 소개했던 Write-Release / Read-Acquire 예제를 atomic_thread_fence 버전으로 바꿔보자.
 
  1. // 쓰레드 1에서의 producer
  2. void TrySendMessage()
  3. {
  4.     //...
  5.    
  6.     g_payload.tick = clock();
  7.     g_payload.str = "TestMessage";
  8.     g_payload.param = param;
  9.  
  10.     // 지금까지 쓴 내용들이 Acquire를 수행한 쓰레드에서 보여져야 한다.
  11.     atomic_thread_fence(memory_order_release);
  12.  
  13.     g_guard.store(1memory_order_relaxed);
  14. }
  15.  
  16. // 쓰레드 2에서 대기중인 consumer
  17. void TryReceiveMessage()
  18. {
  19.     // ...
  20.  
  21.     // Load 수행
  22.     int ready = g_guard.load(memory_order_relaxed);
  23.     if (ready != 0)
  24.     {
  25.         atomic_thread_fence(memory_order_acquire);
  26.         // 이후부터는 Release 이전에 쓰여진 메모리 값들이 모두 제대로 보여야 한다
  27.  
  28.         result.tick = g_payload.tick;
  29.         result.str = g_payload.str;
  30.         result.param = g_payload.param;
  31.     }
  32. }
 
위 코드의 흐름을 그림으로 표현하면 아래와 같다.
 
atomic003.png

 

또 하나의 예를 들자면, 싱글턴에서 자주 사용되는 Double-Checked Locking 기법일 것이다.
 
  1. using namespace std;
  2.  
  3. atomic<Singleton*> Singleton::m_instance;
  4. mutex Singleton::m_mutex;
  5.  
  6. // Double-Checked Locking 기법에 relaxed order와 memory fence 활용
  7. Singleton* Singleton::getInstance()
  8. {
  9.     Singleton* tmp = m_instance.load(memory_order_relaxed);
  10.     atomic_thread_fence(memory_order_acquire);
  11.  
  12.     if (tmp == nullptr)
  13.     {
  14.         lock_guard<mutex> lock(m_mutex);
  15.  
  16.         tmp = m_instance.load(memory_order_relaxed);
  17.         if (tmp == nullptr)
  18.         {
  19.             tmp = new Singleton;
  20.  
  21.             atomic_thread_fence(memory_order_release);
  22.             m_instance.store(tmp, memory_order_relaxed);
  23.         }
  24.     }
  25.     return tmp;
  26. }
 
위 코드의 흐름은 아래 그림과 같다.
 
atomic004.png
 
헌데, 아무리 sequential consistency가 위 방식보다 조금 덜 효율적이라 하지만, 
실제 퍼포먼스 테스트를 해 본 결과 의미를 부여할 만한 성능 차이를 전혀 발견하지 못했다.
 
아래 예제는 C++ atomic default인 memory_order_seq_cst 버전이다. 깔끔!!!
(Intel TBB로 구현했다면, default가 release-acquire)
 
  1. // C++11 디폴트 sequential consistency 버전
  2. Singleton* Singleton::getInstance()
  3. {
  4.     Singleton* tmp = m_instance.load();
  5.     if (tmp == nullptr)
  6.     {
  7.         lock_guard<std::mutex> lock(m_mutex);
  8.  
  9.         tmp = m_instance.load(/*memory_order_seq_cst*/);
  10.         if (tmp == nullptr)
  11.         {
  12.             tmp = new Singleton;
  13.             m_instance.store(tmp, /*memory_order_seq_cst*/);
  14.         }
  15.     }
  16.     return tmp;
  17. }
 
2) atomic_signal_fence(memory_order order)
 
메모리 재배치를 금지한다.
 
4. 추가 링크
 
아래 링크들이 워낙 좋아 추가로 링크를 걸어둔다.
아티클 안에 링크된 페이지들도 읽어 볼 만한 내용이 많으므로, 시간날 때마다 탐독할 것.
(사실 위 내용들의 예제/그림들 중 아래 링크들에서 퍼온 게 많다)
TAG •

List of Articles
No. Subject Author Date Views
23 소켓 통신을 이용한 HTTP 서버 개발 강의록 file digipine 2020.08.01 1482
22 [shared lib] so 동적 라이브러리 만들기와 사용법 - 리눅스 digipine 2017.11.01 6434
21 [linux] zlib build 방법 digipine 2017.11.01 1483
20 [Linux] Pthread 사용법, Thread 동기화 총정리 digipine 2017.11.01 294045
19 [C/C++] 현재시간 구하기 digipine 2017.10.28 2212
18 [C/C++] Random UUID String 생성 코드 digipine 2021.10.21 1302
17 wchar_t에 대하여 digipine 2017.11.01 7343
16 Unix C/C++ Input and Output Function Reference digipine 2017.11.01 88072
15 STL MAP 예제로 공부하기 digipine 2017.10.29 5204
14 Solaris에서 pmap을 이용하여 백그라운드 프로세스 메모리 크기 구하기 digipine 2017.10.29 28598
13 Solaris 10에 개발 Tool (gcc,vim,gdb) 설치 digipine 2017.10.29 1257
12 MD5 파일 변조 검사 관련 소스 (리눅스/Windows) digipine 2017.10.29 2613
11 make -j 옵션으로 컴파일 속도 최적화 하기 digipine 2017.11.01 2759
10 Linux C 언어로 Shell 명령어 실행하기 digipine 2017.11.01 22587
9 Introduce to Singly-linked List file digipine 2017.11.01 1288
8 fopen 파일 열기 모드 옵션 정리 digipine 2017.11.02 3894
7 C를 이용한 객체지향 프로그래밍 digipine 2017.11.01 568
6 Callback in C++ 와 Delegate 차이점 digipine 2017.11.01 2525
5 C++에서 extern의 역할, 기능 digipine 2017.10.29 2656
4 C++ 컴파일 오류(error): variable 'std::istringstream sstream' has initializer but incomplete type digipine 2017.11.02 21077
Board Pagination Prev 1 2 Next
/ 2