KoreanFoodie's Study

[C++ 게임 서버] 1-3. atomic (원자적) 변수와 연산 본문

Game Dev/Game Server

[C++ 게임 서버] 1-3. atomic (원자적) 변수와 연산

GoldGiver 2023. 7. 10. 16:15

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 1-3. atomic 에 대하여

핵심 :

1. ++, -- 같은 간단한 연산조차, 사실 어셈블리어에서는 '값 복사' -> '복사된 값에 연산 수행' -> '복사된 값을 다시 대입' 같은 과정으로 쪼개진다

2. 만약 특정 연산이 진행 중일 경우, 다른 연산이 끼어들지 않도록 만들고 싶다면, 해당 변수를 atomic 으로 선언할 수 있다.

3. 일반적으로 atomic 연산은 일반 연산보다 느리므로, 남용은 지양하여야 한다.

어떤 연산이 atomic (원자적) 으로 이루어진다는 것은, 해당 연산이 진행되는 중간에 다른 연산이 동시에 진행되지 않는다는 것을 의미한다.

이전에 여러 쓰레드들이 공유 자원에 접근하려 할 때 Race Condition 이 발생할 수 있다고 이야기한 적이 있는데, 특정 변수에 1을 더하거나 빼는 간단한 연산조차도, 사실 어셈블리어로 변환하면 여러 줄의 코드가 된다.

 

예를 들어 다음 코드가 있다고 하자 :

using namespace std;

int sum = 0;

void add()
{
	for (int i = 0; i < 100'000; ++i)
	{
		++sum;
	}
}

void sub()
{
	for (int i = 0; i < 100'000; ++i)
	{
		--sum;
	}
}

int main()
{
	thread t1(add);
	thread t2(sub);

	t1.join();
	t2.join();

	cout << "sum : " << sum << endl;
}

싱글쓰레드에 익숙해진 상황이라면, 위 코드에서 sum 의 값은 0 이 될 것이라고 생각하겠지만, 실제로는 다음과 같이 sum 이 0 이 아닌 값이 나오게 된다 :

 

 

디스어셈블러를 보면 다음과 같은데,

 

로직으로 생각하면 다음과 같이 코드가 분해된다 :

따라서, Add 와 Sub 를 하는 쓰레드가 sum 값에 접근할 때는, 실제로 갱신이 된 값을 건드리지 않고, 다른 쓰레드가 아직 연산을 하지 않은 상태의 eax 값에 연산을 할 수 있게 된다.

 

위와 같은 경우는 atomic 변수의 활용으로 해결이 가능하다.

atomic<int> sum = 0;

void add()
{
	for (int i = 0; i < 100'000; ++i)
	{
		sum.fetch_add(1);
	}
}

void sub()
{
	for (int i = 0; i < 100'000; ++i)
	{
		sum.fetch_add(-1);
	}
}

하지만 atomic 연산은 당연하게도 일반 연산보다 느리기 때문에(컴파일러 최적화 여지를 줄이므로), 남용하면 안된다! 🤣

Comments