프로세스와 스레드 - 1

김영웅·2025년 4월 29일

모의 면접을 준비해야되는 상황에서 주제와 맞는 매우 좋은 강의를 해서 하나씩 보면서 익혔다.


프로세스

프로세스는 실행 중인 프로그램을 의미한다
하나의 프로세스는 실행을 위해 독립된 메모리 공간을 할당 받는다.
각 프로세스는 운영체제에 의해 독립적으로 관리되며, 하나의 프로그램이 여러 프로세스를 생성할 수 있다.

프로세스의 구조

프로세스는 운영체제에서 실행되는 동안, 특정한 메모리 구조를 가진다.
프로세스의 메모리 공간은 다음과 같이 나눌 수 있다.


텍스트 세그먼트(코드 세그먼트)

프로그램의 실행 코드를 저장한다.
텍스트 세그먼트는 읽기 전용이다.
C++로 작성된 코드는 컴파일 과정을 거쳐 기계어로 변환되어, 이 기계어 코드가 텍스트 세그먼트에 저장한다.
CPU는 텍스트 세그먼트에 저장된 명령을 가져와 실행한다.

데이터 세그먼트

데이터 세그먼트는 크게 2가지로 나누어진다.
초기화된 데이터와 초기화되지 않은 데이터이다.

  • 초기화된 데이터 세그먼트 (Initialized Data Segment)
    초기화된 전역 변수나 정적 변수를 저장한다.
    C++에서는 static 키워드를 사용한 변수들이 컴파일 시 초기값과 함께 저장되며, 프로그램 시작 시 메모리에 올라간다.

  • 초기화되지 않은 테이터 세그먼트 (BSS, Block Started by Symbol)
    초기화되지 않은 전역 변수나 정적 변수를 저장한다.
    초기값이 명시되지 않은 전역 변수나 정적 변수는 프로그램 실행 시 자동으로 0으로 초기화되며, 이들은 BSS영역에 저장된다.

스택 세그먼트

함수 호출 시 생성되는 지역 변수와 매개 변수를 저장한다.
함수가 호출될 때마다 스택 프레임이 만들어지고 함수가 반환되면 스택에서 해당 프레임이 제거된다.
이러한 스택은 함수 호출이 끝나면 메모리가 자동으로 해제된다.

스택 오버플로우는 함수가 너무 많이 중첩 호출 되었을 때 발생할 수 있다.

힙 세그먼트

동적 메모리 할당이 이루어지는 공간이다.
C++에선 주로 new로 생성된 객체들이 힙에 저장된다.
힙은 스택과 달리 수명이 관리되지 않기 때문에 메모리 누수가 발생할 수 있다.
언리얼에서는 가비지 컬렉션을 통해 일부 객체의 관리를 해줌

가비지 컬렉션

가비지 컬렉션(Garbage Collection)이란 프로그램에서 더 이상 필요 없는 메모리를 자동으로 정리(해제) 하는 시스템이다.

언리얼 엔진은 UObject 기반 클래스에 대해 Garbage Collection 시스템을 제공한다.
언리얼의 GC는 UObject를 상속한 객체들을 추적하여, 더 이상 참조되지 않는 객체를 메모리에서 안전하게 해제한다.

  • 언리얼의 GC가 관리하는 것
    • UObject를 상속한 객체
    • 예시: AActor, UActorComponent, UUserWidget, UTexture, UMaterial, 등
  • GC가 관리하지 않는 것
    • 일반 C++ class (class MyClass {} 처럼 UCLASS() 안 붙은 것)
    • new, malloc 등으로 직접 할당한 메모리 (int*, char*, std::vector 등)
    • FVector, FString, TArray, TMap (이런건 힙 기반이고 직접 관리해야 함)

언리얼의 GC는 객체를 참조하는 다른 객체가 없을 때 해당 객체를 메모리에서 해제한다.
루트 객체(Root Set)부터 시작해서, 참조하고 있는 모든 객체를 추적(Mark)하고, 추적되지 않은(Unreachable) 객체를 정리(Sweep) 하는데 이 과정을 Mark-and-Sweep이라고 부른다.

GC는 게임 틱마다 돌아가는 게 아니라, 메모리 상황이나 GC 트리거 이벤트에 따라 주기적으로 실행된다. (예: 레벨 전환, 특정 조건 충족 시 등)

추가적인 프로세스의 특징

각 프로세스는 독립적인 주소 공간을 갖는다.
이는 하나의 프로세스가 다른 프로세스의 메모리에 직접 접근할 수 없다는 의미이다.

프로세스 간 데이터를 주고받기 위해서는 운영체제의 도움을 받아야 하며, 이를 프로세스 간 통신(Inter-process Communication, IPC)이라고 한다.
IPC 방식에는 파이프, 메시지 큐, 공유 메모리 등이 있다.

운영체제는 멀티태스킹을 지원하기 위해 여러 프로세스를 빠르게 전환하는데, 이를 컨텍스트 스위칭이라고 한다.
각 프로세스의 상태를 저장하고 복원하는 과정에서 오버헤드가 발생할 수 있다.


스레드

스레드는 프로세스 내에서 실행의 흐름을 담당하는 단위이다.
하나의 프로세스에는 하나 이상의 스레드가 존재할 수 있으며, 이들은 같은 메모리 공간을 공유한다.

스레드의 구조

스레드는 같은 프로세스의 메모리 공간을 공유하지만, 각각 독립적인 스택을 가진다.
스레드 간에는 다음 메모리 공간을 공유한다.

  1. 텍스트 세그먼트 : 스레드는 프로세스의 코드를 공유한다.
  2. 데이터 세그먼트 : 전역 변수와 정적 변수를 공유한다.
  3. 힙 : 동적으로 할당된 메모리를 공유한다.

스레드는 같은 프로세스 내에서 자원을 공유하기 때문에 스레드간 전환은 프로세스 간 전환보다 오버헤드가 적다.
즉, 컨텍스트 스위칭이 더 빠르게 이루어진다.

하지만 스레드는 같은 메모리 공간을 공유하기 때문에 동시성 문제가 발생할 수 있다.
두 스레드가 동시에 같은 메모리 위치에 접근하면 경쟁 상태(Race condition)가 발생할 수 있다.
이를 방지하기 위해 락(Lock)이나 세마포어(Semaphore) 같은 동기화 기법이 필요하다.

언리얼에서의 스레드

기본적으로 메인 스레드(Main Thread)가 존재하며, 필요에 따라 추가적인 백그라운드 스레드(Worker Thread)를 생성하여 작업을 분산시킬 수 있다

스레드역할특징
메인 스레드 (Game Thread)게임 로직, 액터 갱신, 블루프린트 실행 등대부분의 게임 코드가 실행
렌더 스레드 (Render Thread)렌더링 명령 생성 및 최적화메인 스레드와 분리되어 렌더링 처리
RHI 스레드 (RHI Thread)GPU 드라이버 통신GPU 명령 최적화 처리
오디오 스레드 (Audio Thread)사운드 재생 및 처리별도 스레드에서 실행
작업 스레드 (Worker Thread)비동기 연산, 파일 로딩, 계산 등필요할 때 생성하여 작업 수행

언리얼 엔진은 멀티스레드로 동작한다.
메인 스레드 외에도 렌더 스레드, 오디오 스레드, RHI 스레드 등이 기본적으로 동작하고 있다
단, 게임 로직(액터 이동, 충돌 처리 등)은 주로 메인 스레드에서 실행된다.


언리얼에서 스레드를 사용하는 방법

  1. Async() 함수 사용 (가장 간단한 방법)

간단한 비동기 작업은 Async() 함수를 통해 쉽게 처리할 수 있다.

#include "Async/Async.h"

void StartAsyncTask()
{
    Async(EAsyncExecution::Thread, []()
    {
        UE_LOG(LogTemp, Log, TEXT("비동기 작업 실행 중입니다."));
    });
}

EAsyncExecution::Thread 옵션을 통해 새로운 스레드에서 작업을 실행한다.
짧고 가벼운 비동기 작업에 적합하다.


  1. FAsyncTask 클래스 사용 (구조화된 비동기 작업)

조금 더 복잡한 비동기 작업이 필요할 때는 FAsyncTask를 사용할 수 있다.

#include "Async/AsyncWork.h"

class FMyAsyncTask : public FNonAbandonableTask
{
public:
    void DoWork()
    {
        UE_LOG(LogTemp, Log, TEXT("FAsyncTask 작업 중입니다."));
    }

    FORCEINLINE TStatId GetStatId() const
    {
        RETURN_QUICK_DECLARE_CYCLE_STAT(FMyAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
    }
};

void StartFAsyncTask()
{
    (new FAutoDeleteAsyncTask<FMyAsyncTask>())->StartBackgroundTask();
}

작업 클래스(FNonAbandonableTask)를 상속하여 작업을 정의한다.
비동기 실행이 완료되면 자동으로 메모리가 정리된다


  1. FRunnableFRunnableThread를 사용한 직접 스레드 제어

스레드를 직접 제어하고 싶을 경우, FRunnable 인터페이스를 구현하여 사용할 수 있다.

class FMyRunnable : public FRunnable
{
public:
    virtual uint32 Run() override
    {
        while (bRunning)
        {
            FPlatformProcess::Sleep(0.1f);
        }
        return 0;
    }

    void StopTask()
    {
        bRunning = false;
    }

private:
    bool bRunning = true;
};

FMyRunnable* MyRunnable = nullptr;
FRunnableThread* MyThread = nullptr;

void StartRunnable()
{
    MyRunnable = new FMyRunnable();
    MyThread = FRunnableThread::Create(MyRunnable, TEXT("MyRunnableThread"));
}

void StopRunnable()
{
    if (MyRunnable)
    {
        MyRunnable->StopTask();
        MyThread->WaitForCompletion();
        delete MyRunnable;
        delete MyThread;
    }
}

직접 스레드를 시작, 정지, 삭제까지 관리해야 한다.
복잡한 커스텀 스레드 작업이 필요한 경우 사용한다.


profile
게임 프로그래머

0개의 댓글