본문 바로가기
개발 (언어)/C++

[MultiThread] 공유 자원, Atomic

by 진현개발일기 2024. 4. 7.

■ 공유데이터 사용 시 발생할 수 있는 문제 상황

아래와 같이 코드를 작성해봤다.

이를 실행 했을 때 결과값은 어떻게 될까? 논리적으로 생각하면 아래와 같이 0이 되어야 한다.

 

하지만 만약 백만번씩 실행한다고 했을 때 여전히 우리가 생각하는대로 0이 될까? 이를 코드 수정 후 확인해보면 아래와 같이 엉뚱한 값이 나오는 것을 볼 수 있었다.

 

 

위에서 공유 영역인 Data영역에 sum 변수가 할당되었다.

이 공유 영역에 할당된 sum 변수를 thread1과 thread2가 같이 사용하기 때문에 발생한 문제이다.

아무리 그래도 더하고 빼는 횟수는 같아야할 텐데 이러한 문제는 왜 발생하는가?

이유는 어셈블리를 까보면  ++ 혹은 --하는 코드는 아래 세 단계로 이루어져 있기 때문이다.

 

[int operator++ assembly 구조]

(1) 전역 변수를 메모리에서 꺼낸 뒤 eax레지스터로 갖고 온다.
(2) 1를 증가 혹은 감소시킨다
(3) 그 값을 다시 제자리로 돌려놓는다.

 

이를 대충 알아볼 수 있게 본문 코드를 수정해본다면 아래와 같은 모습일 것이다.

위와 같은 상황에서 멀티 스레드 환경을 구축한다면 어떠한 스레드가 어떠한 순서로 실행할지 아무도 모른다는 것이다.

그렇기 때문에 값을 다시 대입하는 과정 속에서 만약 다른 스레드가 공유 변수인 sum에 이상한 값을 넣는다면 그 이상한 값 그대로 연산을 수행할 것이다.

 

이를 해결하기 위해선 이전에는 운영체제에서 제공해주는 함수를 따로 사용했었지만, 이는 해당 운영체제 (ex. windows)에 국한되는 함수이고 모던 C++에선 atomic 템플릿을 제공해줌으로써 해결할 수 있도록 지원해주고 있다.

 

 

■ atomic

먼저 atomic을 사용하기 위해선 #include <atomic>을 헤더에 추가해주고 아래와 같이 변수를 선언해줘야 한다.

출력 값을 보면 논리적으로 0의 결과를 반환하는 것을 볼 수 있다.

무슨 원리로 해결이 되었는가 궁금해서 어셈블리를 까보면 아래와 같은 구조를 띄고 있다.

일반 전역변수를 가져다 사용하는 것과 달리 전역 변수를 메모리에서 가져오자마자 std::atomic의 operator++를 실행해주는데 그 내부에는 lock이 된 상태로 실행하다보니 다른 스레드가 연산 과정 중 공유 변수를 사용하는 경우는 없게 되는 것이다.

 

[주의할 점]

그렇다고  모든 변수를 atomic 처리하면 안된다. 그렇게 되면 여러 가지 이슈가 발생할 수도 있지만 무엇보다, atomic의 operator++가 일반 정수 덧셈에 비해 거의 10배 정도 더 무겁기 때문이다. 

 

 

■ 신경써야할 부분

아래와 같이 메인 코드를 작성하고 멀티스레딩을 한다면 무엇이 달라질까? 

load함수는 대입, store은 저장 함수

원래 방식대로 생각한다면 temp에는 sum의 값인 0이 들어가 있어야 한다.

 

하지만 멀티스레드가 환경이라면 temp에 sum의 값을 저장하기 전 sum을 누군가 건드릴 수 있으니 어떠한 값이 들어가있을지 모른다는 것이 주의할 점이다.


만약 함수 대입 전 sum의 값이 무엇이었는지 알고 싶다면 여러 가지 방법이 있겠지만 대표적으로 아래와 같은 방법들로 확인할 수 있다.

int temp = sum.exchange(10); // 10과 교환하면서 이전 값 반환
int prev = sum.fetch_add(30); // 30과 교환하면서 이전 값 반환

■ 요약

공유 변수 사용 시 여러 문제가 발생할 텐데 우리가 꼭 보호해야할 변수는 atomic으로 보호해주면 된다. 하지만 너무 무거운 데이터를 감싸거나 많은 atomic 변수를 선언하고 활용한다면 CPU의 속도는 보장할 수 없다.


또한 이와 같이 멀티스레딩을 할 수 있을려면 어떤 변수 및 함수를 볼때 Heap, Code, Stack, Data 영역 중 어디에 할당되었는지 0.1초만에 직감적으로 알아야 한다. 그리고 아무리 heap, data 영역에 할당되었어도 해당 변수를 공유하고 있지 않으면 딱히 문제가 발생하지 않기에 이를 여러 스레드에서 공유하고 있는가?를 추가적으로 생각해야 한다.

728x90