멀티쓰레드의 이해
멀티쓰레드를 이해하기 위해 예시를 한번 들어보겠다.
여기서 등장인물은 식당, 로봇 직원, 영혼이 있다.
만약 한식, 일식, 레스토랑이 개업을 했다고 해보자.
그리고 한식과 일식은 직원을 1명을 고용했고 레스토랑은 2명을 고용했다. 여기서 직원은 로봇이고 아무런 소프트웨어가 없다. 영혼을 넣어줘야 일을 한다고 해보자.
만약 영혼이라는 것이 세상에 딱 하나밖에 없다고 가정해보자.
그리고 그 영혼을 한식 직원에게 투입했다고 해보자. 그러면 한식 직원만 일을 할 수 있고 나머지 두 식당 직원은 아무것도 못하게 될것이다.
이 것을 해결할 방법을 찾다가 영혼이 모든 직원한테 0.00001초씩 머물고 이동하면 되지 않을까 라는 생각을 하게된다.
즉, 한식 직원한테 아주조금 머물고 일식 직원한테 이동을 하는 것이다. 이것을 아주 빠르게 반복하면 계속 일을 하는 효과를 볼 수 있지 않을까??
이 것이 모든것이라고 얘기할 수 있다.
이것을 식당이 아닌 컴퓨터에 비교해서 생각해보자.
식당을 프로그램, 직원 로봇을 쓰레드, 영혼을 CPU 코어로 바꿔보자.
컴퓨터는 CPU 코어가 쓰레드를 작동시켜야 프로그램이 동작하게 된다. 그런데 그림판만 실행하는게 아니라 메모장 MMO서버 등을 실행하게되면 CPU코어는 한번에 한 쓰레드밖에 동작시키게하지 못하기 때문에 문제가 발생한다. 그래서 위의 영혼이 동작하는 것 처럼 세가지의 프로그램의 쓰레드를 동시에 작동하게 하는 것처럼 보이게 할 수 있다.(컨텍스트 스위칭)
하지만 그림판, 메모장 보단 MMO서버가 더 많은 시간을 쏟아야함으로 운영체제에서 실행 우선순위와 실행시간을 정해주게 된다. (스케쥴링) 그렇게 되다보면 어느 한 프로그램에서 쏠리는 기하현상이 발생하게 된다. 이는 싱글 코어일 때 상황이다.
하지만 요즘은 다중 코어를 사용하기에 그럴 경우는 없다.
그렇다면 식당 직원 즉, 쓰레드를 무작정 많이 늘리는 것은 성능을 향상시킬까?? 이거에 대한 대답은 스스로 생각해보자.
멀티쓰레드에서 각자의 stack영역을 건드리는 것은 아무런 문제가 없지만 공용파트인 heap영역을 건드리다가 잘 못 건드려 프로그램이 꼬이게 될 수 있다.
결론적으로는 식당 직원을 여러명을 두어 식당을 아름답게 운영하되 직원끼리 동선이 꼬이지 않게 관리해야하는 것이다.
쓰레드 생성
이제 비유는 대충 알아 들었을 것이라 생각하고 코드로 돌아와 쓰레드를 생성해보겠다.
using System;
using System.Threading;
namespace ServerCore
{
class Program
{
static void MainThread()
{
Console.WriteLine("Hello Thread!");
}
static void Main(string[] args)
{
Thread t = new Thread(MainThread);
t.Start();
}
}
}
t라고 하는 직원을 고용해 MainThread라는 할일을 주어주고 start를 통해서 영혼을 부여해준것이다. 쓰레드의 생존시기는 여기서는 Main함수와 같다.
그런데 만약 MainThread에 무한루프를 넣어 멈추지 않게하면 어떻게 될까?? 프로그램이 계속 Hellow Thread를 출력할 것이다. C++이랑 다르게 C#에서는 기본적으로 isBackground가 false로 설정되어 foreGround로 실행된다. 즉 메인쓰레드가 종료되어도 계속 실행하는 것이다. (true로 설정하면 Main이 멈출때 같이 멈춘다.)
t의 쓰레드가 종료 될 때까지 기다리기 위해서는 Join을 사용하면 된다.
쓰레드를 만드는 과정은 정말 큰 부하를 일으킬 수 있다. 즉, 직원을 정규직으로 고용한 것이다. 하지만 그것이 아닌 알바처럼 아주 간단한 일만 하고 사용하지 않게끔 하고 싶을 때가 있다. 그럴때마다 Thread t = new Thread 같은것은 매우 비 효율적이다. 그렇기 때문에 ThreadPool을 이용한다.
using System;
using System.Threading;
namespace ServerCore
{
class Program
{
static void MainThread(object state)
{
Console.WriteLine("Hello Thread!");
}
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(MainThread);
}
}
}
그렇다면 ThreadPool은 어떤 원리일까? new Thread를 사용할 경우 우리가 모든 직원의 책임을 보장해야한다. 하지만 ThreadPool 같은 경우 이미 사대보험이나 여러 안전사항들을 마련되어있고 인력대기소에 알바만 보내서 일만 하고 나오는 것이라고 생각하면 된다.
ThreadPool은 한번에 실행할 수 있는 쓰레드의 갯수가 한정되어 있다. 이게 장점이자 단점이다.
즉, 5개의 일이 최대인데 내가 6개의 일을 시켰을 경우 프로그램이 먹통이 될 수 있다.(5개의 일이 무한루프이다.)
또 다른 방법으로는 Task를 사용하는 방법이있다. Task는 유연하게 Thread를 직접 관리할지 ThreadPool에서 관리할지 결정할 수 있다. 우리가 C#에서 직접 쓰레드를 만들어서 작업하는 일은 매우 적을 것이다. 대부분의 경우 ThreadPool에서 관리한다.
Task t = new Task(() => { while (true) { } }, TaskCreationOptions.LongRunning);
t.Start();
컴파일러 최적화
만약 다음과 같은 코드가 있다고 생각해보자. 디버그 모드에서 실행해보자.
using System;
using System.Threading;
namespace ServerCore
{
class Program
{
static bool _stop = false;
static void ThreadMain()
{
Console.WriteLine("스레드 시작");
while(_stop == false)
{
// TODO
}
Console.WriteLine("스레드 종료");
}
static void Main(string[] args)
{
Task t = new Task(ThreadMain);
t.Start();
Thread.Sleep(1000);
_stop = true;
Console.WriteLine("Stop 호출");
Console.WriteLine("종료 대기중");
t.Wait();
Console.WriteLine("종료 성공");
}
}
}
아무런 문제 없이 코드가 성공적으로 작동하는 모습을 볼 수 있다.
그 이유는 당연하게도 싱글쓰레드를 사용하기 때문이다. 또한 디버그 모드로 실행했기 때문에 문제가 없는 모습을 볼 수 있다.
만약 릴리즈 모드에서 코드를 실행해보자. 프로그램이 종료되지 않고 무한루프를 도는 모습을 볼 수 있다.
릴리즈 모드에서는 컴파일러가 자체적으로 코드를 읽고 최적화를 진행해준다.
while(_stop == false)
{
// TODO
}
// 컴파일러 최적화를 진행하면 밑으로 변환
if(_stop == false)
{
while (true)
{
// TODO
}
}
이렇게 변환되기 때문에 메인함수에서 _stop을 true로 설정할지라도 멈추지 않는 것이다.
어셈블리어로 확인해봐도 같은 모습을 볼 수 있다.
이 경우 컴파일러의 최적화가 오히려 우리가 의도한 방식을 방해한 것이다.
그렇기 때문에 이럴 경우에는 volatile 키워드를 사용해 처리할 수 있다.
volatile static bool _stop = false;
_stop 변수를 최적화 하지 말라는 의미이다.
여담이지만 c++에서도 volatile 키워드가 존재하는데 의미가 비슷하면서도 다르다. 그리고 c#에서도 volatile를 사용하는 것을 매우매우매우 지양한다. 다른 메모리배리어 라던지 lock을 사용해서 처리하는 것을 훨씬 권장하므로 지금처럼 이런 현상이 있구나 정도로만 기억하면 된다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[게임 서버] 네트워크 프로그래밍 - 네트워크 기초 및 소켓 프로그래밍 (0) | 2024.01.17 |
---|---|
[게임 서버] 멀티쓰레드 프로그래밍 - Context Switching, Event등 Lock의 다양한 기법 (1) | 2024.01.11 |
[게임 서버] 멀티쓰레드 프로그래밍 - Lock에 대해서 (1) | 2024.01.10 |
[게임 서버] 멀티쓰레드 프로그래밍 - 캐시, 메모리 배리어, Interlocked (1) | 2024.01.09 |
[게임 제작] 온라인 rpg 제작 준비 (2) | 2024.01.08 |