-
C++ 멀티스레드, AtomicC++/C++ 멀티스레드 2022. 10. 5. 23:22728x90
멀티스레드 환경에서의 작업은 동시에 다양한 작업을 가능케 하지만,
여러가지 문제도 존재하고, 그러한 문제들이 멀티스레드 프로그래밍을 어렵게 한다.
여러가지 문제점들 중 한 가지가 공유데이터에 대한 동시접근시 발생하는 문제다.
#include <thread> #include <iostream> #include <atomic> int sum = 0; void Add() { for (int i = 0; i < 100000; i++) { sum++; } } void Sub() { for (int i = 0; i < 100000; i++) { sum--; } } void main() { Add(); Sub(); cout << sum << endl; }
이런 코드가 있다고 하자.
결과는 당연하게도 0이 나온다.
이 코드를 멀티스레드 환경에서 실행해보자.
void Add() { for (int i = 0; i < 100000; i++) { sum++; } } void Sub() { for (int i = 0; i < 100000; i++) { sum--; } } void main() { std::thread t1(Add); std::thread t2(Sub); t1.join(); t2.join(); cout << sum << endl; }
이렇게 실행하면 결과는 0이 아닐뿐더러, 실행할 때마다 다른 결과가 나온다.
그 이유에 대해서 살펴보자.
sum++과 sum--는 cpp코드에서는 한 줄로 실행되는 것처럼 보이지만, 실제로는 그렇지 않다.
어셈블리코드를 열어보면 알 수 있지만, 간단하게 설명하자면
1. sum값을 가져와서 임시변수 A에 저장한다.
2. A에 저장된 값을 1 증가시킨다.
3. A의 값을 sum에 저장한다.
A = sum
A = A + 1
sum=A
이러한 과정으로 sum++가 실행된다.
그러다 보니 여러 스레드에 의해서 공유변수 sum에 대해 sum++과 sum--가 동시에 실행되면
아래와 같은 문제가
발생한다.발생할 수 있다.No 스레드 t1 A 스레드 t2 B sum 1 A=sum 0 0 2 B=sum 0 0 3 A=A+1 1 4 sum=A 1 5 B=B-1 -1 6 sum=B -1 sum을 한 번 더하고, 한 번 뺐으면 결과로 0이 나와야 할 것 같은데
sum은 -1이 나온다.
이 결과는 스레드가 각 명령을 실행하는 순서에 따라서 0이 될수도 있고, 1이될수도, -1이 될 수도 있다.
여튼... 공유데이터에 대한 접근에 대해서 관리를 해주지 않으면, 데이터가 의도한대로 나오지 않는 문제가 발생할 수 있다.
다양한 해결 방법이 있지만 여기서는 atomic에 대해서 살펴보겠다.
Atomic
All or Nothing
무슨 뜻이냐면, 전부 실행하거나 하나도 실행하지 않거나 라는 뜻이다.
앞서 sum++은 실제로
1. A = sum
2. A = A + 1
3. sum=A
세 단계로 실행된다는 것을 알아봤다.
Atomic하게 실행하면, 이 세 단계를 전부 실행하거나 실행하지 않는다는 의미이다.
즉, sum++의 2단계정도 수행하고, 다른 스레드가 sum에 대해서 접근하려고 하면,
sum++에 대한 연산을 끝낼때 까지 다른 스레드의 접근을 막거나(all)
sum++가 지금까지 했던 연산 전부를 취소하는 것이다(nothing)
예를 들어보자
M이라는 게임에서 두 유저가 교환을 한다고 해보자
A가 B의 아이템을 100 게임머니에 사는 상황이라면
1. A가 B에게 100게임머니를 지급함
2. B가 A에게 아이템을 지급함
이렇게 설명할 수 있을 것이다.
이러한 작업을 처리할 때는 Atomic하게 실행해야한다.
만약 Atomic하게 하지 않았는데 A가 B에게 100 게임머니를 지급하는 순간 서버가 다운됐다면,
A는 100게임머니를 잃었는데, 그 100게임머니는 B에게 들어가지도 않은 상황이 될것이다.
그러면 C++에서는 어떻게 구현하는지 알아보자.
간단하다 변수 선언부만 조금 바꿔주면 된다.
atomic<int> sum; void Add() { for (int i = 0; i < 100000; i++) { sum++; } } void Sub() { for (int i = 0; i < 100000; i++) { sum--; } } void main() { std::thread t1(Add); std::thread t2(Sub); t1.join(); t2.join(); cout << sum << endl; }
이렇게 하면, 여러번 실행하더라도 결과가 0이 나온다.
추가로 atomic을 사용할 때 사용하는 함수가 몇 가지 더 있다.
atomic<int> sum; void Add() { for (int i = 0; i < 100000; i++) { sum.fetch_add(1); } } void Sub() { for (int i = 0; i < 100000; i++) { sum.fetch_sub(1); } } void main() { std::thread t1(Add); std::thread t2(Sub); t1.join(); t2.join(); cout << sum << endl; }
fetch_add와 fetch_sub와 같이 atomic변수가 사용할 수 있는 전용 덧셈 뺄셈이다.
사용하지 않아도 결과가 잘 나온다.
이 외에도 and, or, xor등의 함수도 있다.
그럼 공유데이터에 대한 접근시 atomic만 쓰면 되나??
꼭 그렇지는 않다.
atomic 변수에 대한 연산은 속도가 상당히 느리다.
뿐만 아니라 이것 말고도 다른 동기화 기법들도 존재한다.(고 한다)
여튼 atomic변수는 꼭 필요할 때만 사용하는게 좋겠다.
728x90'C++ > C++ 멀티스레드' 카테고리의 다른 글
C++ SpinLock구현 (0) 2022.10.09 멀티스레드 Lock구현 (0) 2022.10.09 C++, 멀티스레드 교착상태(DeadLock) (0) 2022.10.08 C++ 멀티스레드, lock(mutex) (1) 2022.10.06 C++ 스레드 생성 (0) 2022.10.05