게임서버) 3.멀티쓰레드 공유자원
멀티 쓰레드 공유자원
공유 자원은 여러 프로세스가 공동으로 이용하는 변수, 메모리, 파일 등을 말한다. 공유 자원은 공동으로 이용되기 때문에 누가 어떻게 데이터를 읽거나 쓰느냐에 따라 결과가 달라질 수 있다. 이러한 공유자원을 이해하기 위해서는 일단 메모리의 구조를 이해해야된다.
메모리구조
메모리는 총 4가지 구간으로 나눌수 있는데, 각각의 영역은 다음과 같은 역할을 수행한다.
-코드영역: 코드영역은 실행할 프로그램의 코드가 저장되는 영역으로 텍스트영역이라고도 부른다.CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리하게 되며, 공유하더라도 문제가 되지않는다. 단, 우리가 적은 코드 순서대로 실행되는 것이 아닌 CPU파이프라인에 영향을 받아 순서가 다소 다를수 있다.
-스택 영역:스택영역은 함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역이다.스택 영역은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸한다.이렇게 스택 영역에 저장되는 함수의 호출 정보를 스택 프레임이라고 한다.이러한 스택은 LIFO방식에 따라 동작하므로, 가장 늦게 저장된 데이터가 가장 먼저 인출된다.스택 영역은 메모리의 높은 주소에서 낮은 주소의 방향으로 할당되며, 공유하더라도 문제가 되지 않는다.
-데이터영역: 데이터영역은 프로그램의 전역 변수와 Static변수가 저장되는 영역이다. 데이터 영역은 프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸한다. 데이터영역을 공유할 경우 문제가 발생할수 있다.
-힙 영역:힙 영역은 사용자에 의해 메모리 공간이 동적으로 할당되는 영역을 말한다.힙영역은 프로그래머가 관리를 해야되는 영역이며, 메모리의 낮은 주소에서 높은 주소의 방향으로 할당된다. 힙영역의 경우 공유하게 되면문제가 발생할수 있다.
멀티쓰레드 환경에서 공유시 문제 여부
-코드영역(X)
-스택영역(X)
-데이터영역(O)
-힙 영역(O)
멀티쓰레드 환경에서 공유자원의 문제점
위의 메모리구조와 같이 데이터 영역과 힙영역은 공유할 경우 문제가 발생할수 있다고 하였다. 그렇다면 어떠한 문제가 어떤 식으로 발생하는 것인가?
void Thread()
{
for (int i = 0; i < 100'0000; i++)
{
sum++;
}
}
void Thread2()
{
for (int i = 0; i < 100'0000; i++)
{
sum--;
}
}
int main()
{
thread t1(Thread);
thread t2(Thread2);
t1.join();
t2.join();
}
보기에는 sum++하나의 코드로 되어있지만 실제로는 sum++을 디스 어셈블리하면 다음과 같은 코드로 나온다
즉 아래와 같은 코드처럼 행동한다는 뜻이다.
int eax=sum;
eax=eax+1;
sum=eax;
이럴 경우 멀티쓰레드 환경에서 다음과 같은 경우가 발생할수도 있게된다.
---------------------------Thread시작
int eax=sum;// eax=0
eax=eax+1; //eax=1
----------------------------Thread2시작
int eax=sum;// eax=0
eax=eax-1; //eax=-1
sum=eax; //sum=-1
----------------------------Thread2종료
sum=eax; //sum=1
---------------------------Thread종료
실행결과
본래라면 결과값0이 나왔어야 되지만 멀티쓰레드 환경에서 데이터/힙영역의 메모리를 공유하면 이러한 현상이 발생하며 이러한 현상을 레이스컨디션이라고 한다.
힙영역 공유자원으로 혼동 할 수 있는 구조
void Test()
{
for(int i=0;i<100'000;i++
{
int *p=new int();
*p=100;
delete p;
}
}
언뜻 보면 동적할당을 통한 공유자원으로 생각 될수 있지만, 전역변수가 아닌 p는 스택 영역안에 존재하므로 멀티 쓰레드 환경에서 레이스 컨디션이 일어나더라도 문제가 발생하지 않는다.
문제가 발생할수 있는 구조는 스택에서의 포인터가 아닌 매개변수와 같이 공유하는 포인터를 사용할 경우 문제가 발생하므로 이부분을 주의해야된다.
//문제가 발생할수 있는 코드
void Test(int *p)
{
for(int i=0;i<100'000;i++
{
*p=new int();
*p=100;
delete p;
}
}
int main()
{
int *p=new int();
//------------같은 포인터를 공유하고 있음-----------
thread t1(Test,p);
thread t2(Test,p);
//------------같은 포인터를 공유하고 있음-----------
t1.join();
t2.join();
}
공유자원 관리
이러한 레이스 컨디션과 같은 현상들이 일어날수 있기 때문에 공유자원은 관리가 필요하다. 공유자원관리는 대표적으로 임계구역 설정과 Atomic 변수설정등이 있다. 이번 포스팅에서는 Atomic변수 설정에 대해 다루고자 한다.
Atomic변수 설정
Atomic은 변수에 락을 걸어주는 개념이다. 본래는 운영체제마다 다르게 설정되는 변수로써 Window의 경우 InterlockedIncrement(&변수)였으나 C++11에서 Atomic클래스가 새로 추가되었다.
Atomic생성
#include<atomic>
atomic<변수타입>sum=0;
Atomic 적용
#include<atomic>
atomic<int>sum=0;
void Thread()
{
for (int i = 0; i < 100'0000; i++)
{
sum++;
-----atomic에서 동일한 방식----
//int temp=sum.load();
//temp=temp+1;
//sum.store(temp);
----------자체 함수---------
//sum.exchange(sum+1)==sum.fetch_add(1)
}
}