티스토리 뷰

이전 글에서 스레드에 대해 이해했다. 이번 장에서는 멀티스레드 프로그래밍이 어디서 어떻게 사용될지 알아보자.

 

 


멀티스레드는 언제 사용되는가?

단일 스레드에서 작업하던 사람들은 멀티 스레드를 접하면 "동시에 여러 작업이 한번에 처리된다."라는 개념에 신선함을 느낄 수 있지만, 멀티스레드는 남용되면 굉장히 골치가 아파진다.

 

이는 멀티스레드를 어떻게 사용하는가에 따라 단일 스레드보다도 못한 성능이 나올 수도 있기도 하며, 심각하게는 조금만 실수해도 프로그램이 중단되는 오류가 될 수도 있기 때문이다.

 

그렇다면 멀티스레드는 어디서 사용되야하는가? 대표적으로는 다음과 같은 상황들이 있다.

 

  1. 오래 걸리는 일 하나빨리 끝나는 일 여럿동시에 처리하는 경우
  2. 긴 작업을 진행하는 동안 다른 짧은 일을 처리하는 경우
  3. 기기의 CPU를 모두 활용해야할 경우

 


오래걸리는 일 하나와 빨리 끝나는 일 여럿을 동시에 처리할 경우

이러한 경우는 예로 게임의 로딩(loading)과 같은 작업이 이루어질 경우이다.

 

게임은 실행 후 플레이하기 까지 굉장히 무거운 데이터들을 한 번에 호출하여 메모리에 올려야 한다. 하여 캐릭터, 배경, UI 등의 그래픽 리소스를 로딩하기 위해 시간이 어느정도 필요하다.

 

이러한 데이터를 읽어오는 과정 동안 화면은 멈추어있다면 사용자에게 지루함과 현재 프로그램이 제대로 동작은 하고 있는지 얼마나 더 기다려야하는지 확인할 방법을 제공해주지 않는다. 하여 개발자들은 로딩바를 통해 진행 정도를 GUI로 표현하여 사용자로써 게임이 실행될 예상시간을 기대하게 하고 프로그램이 오류가난 것이 아닌 데이터를 로딩 중이라는 메시지를 전달한다.

 

물론 필수적으로 숫자나 로딩바로 진행 상황을 알려줘야하는 것은 아니고, 로딩중 미니게임이나 작은 애니메이션을 통해 "게임이 비정상적으로 정지되지 않았다"정도의 메시지를 전달해도 된다.

 

잠시 게임 이야기로 빠져버렸다. 본론으로 다시 돌아가자.

 

 

이러한 로딩바를 멀티스레딩이 존재하지 않는다면 아마 다음과 같이 코드를 작성해야할 것이다.

 

렌더링 데이터 로딩
LoadScene() {
    Render();
    LoadModel();
    Render();
    LoadTexture();
    Render();
    LoadAnimation();
    Render();
    LoadSound();
}

 

위의 끔찍한 코드는 단일 메인스레드로 한 화면을 렌더링하려는 중이다. 여기서 만약 로딩해야하는 파일의 크기가 거대하다면 큰 파일을 로딩하는 동안 일시적 프레임 드랍이 발생할 것이다. 청크(chunk)단위로 분해하여 렌더링을 시도할 수도 있지만 코드는 더 길고 지저분해질 것이며 여전히 문제가 있을 것이다.

 

이러한 경우 다음과 같이 스레드를 사용해 볼 수 있다.

 

스레드를 사용한 렌더링 데이터 로딩
#include <iostream>
#include <thread>

using namespace std;

bool isStillLoading = true; // 전역 변수

void LoadScene() { cout << "씬 로딩 로직" << endl; }
void LoadModel() { cout << "모델 로딩 로직" << endl; }
void LoadTexture() { cout << "텍스처 로딩 로직" << endl; }
void LoadAnimation() { cout << "애니메이션 로딩 로직" << endl; }
void LoadSound() { cout << "음향 로딩 로직" << endl; }

void FrameMove() { cout << "프레임 이동 로직" << endl; }
void Render() { cout << "렌더링 로직" << endl; }

void Minigame() {
    isStillLoading = true;
    
    // 로딩 중 미니게임 렌더링
    while (isStillLoading) {
        FrameMove();
        Render();
    }
}

void Loading() {
    LoadScene();
    LoadModel();
    LoadTexture();
    LoadAnimation();
    LoadSound();

    // 로딩 종료를 알리기 위해 isStillLoading 값을 변경
    isStillLoading = false;
}

int main() {
    std::thread load(Loading);    // 로딩 진행
    std::thread mini(Minigame);   // 미니게임 진행
    
    // 스레드들이 종료될 때까지 대기
    load.join();
    mini.join();

    return 0;
}

 

위 코드를 실행하면 미니게임을 위한 로직과 렌더링이 진행되는 와중에도 데이터 로딩이 진행되는 것을 알 수 있다. (결과가 너무 줄이 길기 때문에 결과는 생략한다.)

 

 


어떤 긴 처리를 진행하는 동안 짧은 일을 처리할 경우

데이터를 보조 기억 장치인 디스크에서 읽거나 써야하는 경우가 있을 수 있다. 이러한 경우 디스크 액세스가 필요한데 컴퓨터를 이루는 각 하드웨어는 CPU가 직접 조작할 수 없기 때문에 디스크마다 있는 디스크장치라는 중간 장치를 거쳐야한다. 이러한 과정만 들어도 메모리에 올라와 있는 데이터를 처리하는 것보다 디스크에서 데이터를 읽어 오는 것이 훨씬 느리다는 것을 예상할 수 있다.

 

하여 이러한 데이터 처리 기간동안 대기상태가 될 수 있는 CPU를 놀도록 놔둘 수 는 없기에 멀티스레딩을 통해 프로세스의 다른 기능을 구동하도록 만들어야 한다.

 

물론 평균 하드디스크 액세스 정보가 1만분의 1초라는 굉장히 짧은 시간이라지만 30 FPS가 나오기 위해 1초에 30만 번의 연산을 해야하는 컴퓨터에게 1초에 1만번이면 굉장히 느린 것이다.

 

 


기기의 CPU를 모두 활용해야하는 경우

컴퓨터 부품의 발전 속도는 기하급수적으로 발전하고 있다. CPU 역시 기존의 단일 코어에서 현재는 64코어가 탑제된 CPU까지 있다. (물론 일반적으로 사용되는 데스크탑용 CPU는 아니다.)

 

이러한 발전은 프로그램의 성능을 높일 수도 있겠지만, 반대로 원래 싱글스레드 프로그램이 존재한다면 컴퓨터의 코어 개수와 상관없이 1개의 코어만 사용하게 된다는 장단점이 포함된다.

 

즉, 서버에 코어가 8개인 CPU를 장착했는데, 싱글스레드 게임 서버를 만든다면 서버 전체의 연산을 $\frac{1}{8}$ 성능으로 사용하는 것이다.

 

예를 들면, 1~800만 사이에 특정 숫자들을 선형 탐색해야 한다고 하자. (편의상 알고리즘 없이한다고 가정합시다.)
이때, 코어가 8개인 컴퓨터로 탐색을 한다면, 각 코어마다 하나의 스레드로 100만개씩 탐색하게 800번의 탐색 시간을 $\frac{1}{8}$으로 줄일 수 있다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함