C++/C++ 멀티스레드

C++ 멀티스레드, Atomic

박성장 2022. 10. 5. 23:22
728x90

멀티스레드 환경에서의 작업은 동시에 다양한 작업을 가능케 하지만,

여러가지 문제도 존재하고, 그러한 문제들이 멀티스레드 프로그래밍을 어렵게 한다.

 

여러가지 문제점들 중 한 가지가 공유데이터에 대한 동시접근시 발생하는 문제다.

#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