[C#] 스레드를 생성해보자

Arthur·2023년 9월 17일
0

공부하게된 계기


게임 서버 개발 공부를 하는 중 제가 너무 클라이언트(유니티) 관련 구현에 치중된 것을 알게되었습니다.
그래서 다시 게임 서버 관련 강의를 들으면서 정리하기 위해 글을 작성하게 되었습니다.

게임 서버에서 멀티 스레드를 사용해 처리 속도의 효율성을 올린다고 합니다.
우선 멀티 스레드에 대해 이해하기 전에 스레드에 대해 알기위해 공부하게 되었습니다.

C#으로 게임 서버를 만들어보고 있기 때문에 예제코드는 C# 관련 스레드 코드로 작성되어있습니다.



스레드란?


어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 실행 방식을 멀티스레드(multithread)라고 한다.
<위키백과 - 스레드(컴퓨팅)>

프로세스는 컴퓨터에서 실행되고 있는 컴퓨터 프로그램을 말합니다.
운영 체제에서는 이 프로세스를 관리하며, 각 프로세스는 자체 메모리 공간과 실행 상태,
프로그램 카운터 등의 정보를 가집니다.

요즘은 대부분 멀티 스레드 방식을 사용해 프로그램을 만들고 있습니다.
특히 게임 서버 쪽은 이런 멀티 스레드의 중요성이 상당히 큽니다.

각 플레이어의 액션, 게임 내의 이벤트, 데이터 동기화 등 다양한 작업들을 동시에 처리해야 하기 때문입니다.

C#에서는 어떻게 스레드를 생성할 수 있을지 코드를 찾아봤습니다.



C# 스레드 생성 예제 코드


class Program
{
	// 스레드가 실행할 메서드
    static void MainThread()
    {
        Console.WriteLine("Hello Thread!");
    }

    static void Main(string[] args)
    {
    	// 스레드 생성 시 인자로 실행할 함수를 꼭 넣어줘야 합니다.
        Thread thread = new Thread(MainThread);
        // 생성한 스레드를 실행합니다.
        thread.Start();
    }
}

코드는 생각했던 것보다 훨씬 간단합니다.
위 코드를 실행하면 아래와 같이 콘솔 창에 출력이 됩니다.

여기서 C#에서 스레드를 생성할 때의 특징이 있다고 합니다.

  • 관리되는 스레드는 백그라운드(background) 스레드 또는 포그라운드(Foreground) 스레드입니다.
    • 백그라운드 스레드 : 메인 스레드가 종료되면 바로 프로세스를 종료합니다.
    • 포그라운드 스레드 : 메인 스레드가 종료되더라도 포그라운드 스레드가 살아 있는 한, 프로세스가 종료되지 않고 계속 실행됩니다.
  • C#에서는 기본적으로 스레드 생성 시 포그라운드 스레드로 생성됩니다.
  • 스레드 풀에 속하는 스레드는 백그라운드 스레드입니다.

백그라운드 스레드로 사용할 지, 포그라운드 스레드로 사용할지는 직접 선택할 수 있습니다.

백그라운드 스레드

class Program
{
    static void MainThread()
    {
        while (true)
            Console.WriteLine("Hello Thread!");
    }

    static void Main(string[] args)
    {
        Thread thread = new Thread(MainThread);
        thread.IsBackground = true;
        thread.Start();

        Console.WriteLine("대기중");
        Console.WriteLine("스레드 종료");
    }
}

위 코드의 출력 결과를 보면 아래와 같습니다.

백그라운드 스레드는 메인 스레드가 종료되면 종료 하는 것을 알 수 있습니다.


포그라운드 스레드

class Program
{
    static void MainThread()
    {
        while (true)
            Console.WriteLine("Hello Thread!");
    }

    static void Main(string[] args)
    {
        Thread thread = new Thread(MainThread);
        thread.IsBackground = false;
        thread.Start();

        Console.WriteLine("대기중");
        Console.WriteLine("스레드 종료");
    }
}

위 코드의 출력 결과를 보면 아래와 같습니다.

반면에 포그라운드 스레드는 메인 스레드가 종료 되어도 while 반복문이 계속 실행됩니다.

❓백그라운드와 포그라운드 스레드로 나눈 이유는 뭘까?

포그라운드 스레드는 주로 사용자 인터페이스와 관련도니 작업에 사용되며,
백그라운드 스레드는 비동기 및 보조 작업에 사용됩니다.
이렇게 스레드를 나눔으로써 병렬성을 활용하면서도 안정성을 유지할 수 있습니다.



그렇다면 이런 스레드를 어떻게 관리할까?


멀티 스레드 프로그래밍에서 중요한 것 중 하나는 스레드를 관리하는 것에 있습니다.

단순히 스레드를 많이 생성해서 작업자들이 많아져 성능이 향상되지 않습니다.
고려해야 할 부분과 까다로운 점이 많다고 합니다.

  • 스레드를 생성했는데 특정 스레드에만 작업이 몰리는 경우
  • 스레드를 많이 생성해서 작업을 수행하는데 컨텍스트 스위칭이 많이 발생하는 경우
  • 스레드가 공유하는 자원에 대한 문제점
    • 경쟁 조건(Race Condition) : 두 개 이상의 스레드가 공유 자원에 접근하고 수정을 시도할 때 경쟁 조건이 발생하며, 이로 인해 예상치 못한 결과가 발생할 수 있다.
    • 데드락(Deadlock) : 두 개 이상의 스레드가 서로의 작업이 끝나기를 기다리며 블록될 때.
    • 교착 상태(Starvation) : 한 스레드나 그룹이 지속적으로 자원을 사용하여 다른 스레닥 자원에 접근하지 못하게 하는 상태
    • 데이터의 일관성 문제 : 여러 스레드가 공유 자원을 동시에 읽고 쓸 때 데이터의 일관성 문제가 발생한다. 한 스레드가 공유 자원을 읽어오는 데 다른 스레드가 데이터를 수정할 경우가 그 예시다.
    • 캐시 일관성 문제 : 다중 코어 CPU에서 실행 중인 여러 스레드가 메모리의 복사본을 가지고 있을 때, 캐시 일관성 문제가 발생할 수 있어, 스레드가 올바른 데이터를 읽지 못할 수 있습니다.

개발자가 직접 멀티 스레드를 관리한다는 것이 상당히 어렵다는 것을 알게되었습니다.
이런 어려움을 어느정도 도움을 주기 위해 C#에서는 스레드 풀



C# ThradPool


스레드 풀이란 스레드를 미리 생성하고, 작업 요청이 발생할 때마다 미리 생성된 스레드로 해당 작업을 처리하는 방식을 의미합니다.

C#에서는 이런 스레드 풀을 제공해줍니다.
그래서 직접 구현하는 번거로움을 덜어줍니다.

예제코드

class Program
{
    static void MainThread(object state)
    {
        for (int i = 0; i < 5; i++)
            Console.WriteLine("Hello Thread!");
    }

    static void Main(string[] args)
    {
    	// 스레드풀에 있는 스레드를 이용하여 MainThread() 메서드 실행
        ThreadPool.QueueUserWorkItem(MainThread);
        
        Console.WriteLine("Finish");
    }
}

스레드풀의 장 단점

  • 장점 : CPU의 자원에 맞춰서 일정량의 스레드를 생성해서 사용합니다. 그래서 재사용성이 좋고, 자원을 효율적으로 사용할 수 있습니다.
  • 단점 : 하나의 작업(Task)이 스레드를 계속 잡고 있으면 스레드 풀이 먹통이 될 수 있다.

스레드풀 먹통으로 만들기

class Program
{
    static void MainThread(object state)
    {
        for (int i = 0; i < 5; i++)
            Console.WriteLine($"Hello Thread!");
    }

    static void Main(string[] args)
    {
        ThreadPool.SetMinThreads(1, 1);
        ThreadPool.SetMaxThreads(5, 5);

        for (int i = 0; i < 4; i++)
            ThreadPool.QueueUserWorkItem((obj) => { while (true) { } });

        ThreadPool.QueueUserWorkItem(MainThread);
        
        Console.WriteLine("Finish");
    }
}

사용할 스레드를 세팅하고 그 무한 반복문 작업을 실행시켜 먹통을 만드는 코드입니다.

실행을 시키면 위와 같이 스레드풀에서 실행 할 MainThread 작업은 실행되지 않고 종료가 됩니다.

이렇듯 스레드풀을 사용할 때는 작업의 길이를 잘 고려해서 사용해야 합니다.



작성하면서 느낀점


자바, 스프링을 사용하면서 스레드를 직접 생성하거나 스레드를 고려해서 프로그래밍을 해본적이 없었습니다.
이번에 게임 서버 프로그래밍을 공부 하면서 직접 만들어보면서 이해가 훨씬 잘 되었습니다.

그리고 고려해야 할 부분과 얼마나 번거로운지 매번 알게 됩니다.

스레드풀이 이런 번거로움을 보완한다고 하지만, 스레드풀도 잘못 사용하면 먹통이 되어서 역효과가 발생한다는 것도 알았습니다.

이번에 공부를 하면서 프로세스와 스레드의 차이도 명확하게 이해를 하게 되었습니다.
확실히 코드를 직접 작성하고 출력을 해서 확인하는 단계가 정말 중요한 것 같습니다.



참고 자료


  • [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 => 링크
  • csharpstudy.com - C# 쓰레드의 생성 (Thread 클래스) => 링크
  • csharpstudy.com - C# ThreadPool 사용 => 링크
  • 위키피디아 - 스레드(컴퓨팅) => 링크
  • MS Docs - 포그라운드 및 백그라운드 스레드 => 링크
profile
기술에 대한 고민과 배운 것을 회고하는 게임 서버 개발자의 블로그입니다.

0개의 댓글