프로세스, 스레드 Let's Go
서론
프로세스, 스레드가 뭘까? 이것들의 정의와 실제로 쓰이는 것들인가?
어디서, 어떻게 쓰일까?
개요
프로세스란?
프로세스(Process)는 컴퓨터 시스템에서 실행 중인 프로그램의 단위를 말한다.
즉, 운영체제에서 독립적으로 실행되는 작업의 단위이며, 독립적인 메모리 공간과 자원을 할당받아 실행된다.
따라서, 한 번에 하나씩의 프로세스만 실행되고 다른 프로세스와 완전히 분리되어 있다. 프로세스는 자신만의 메모리 공간을 가진다. 이때 메모리 공간은 (Stack, Heap, Data, Code) 영역으로 나뉜다.
스레드란?
프로세스 안에 있는 실행 흐름의 단위로, 프로세스 내 자원을 공유하고 병렬 작업을 수행한다. 즉, 하나의 프로세스 안에 여러 개의 스레드가 존재할 수 있으며, 이 스레드는 공유 자원을 통해 병렬 작업을 수행한다.
이때 공유 자원이란, 메모리 공간 내에서 Heap, Data, Code을 의미한다. 그리고 각각의 스레드는 고유의 Stack공간을 가진다.
본론
Unreal Engine과 Unity 같은 상용 게임 엔진은 물리, 렌더링, 네트워크 작업에서 자동으로 멀티스레딩을 활용해 최적화를 제공한다. 덕분에 개발자는 복잡한 스레드 관리 없이도 게임을 설계할 수 있지만, 특정 비동기 작업에서는 직접 스레드를 제어해야 할 경우도 있다.
스레드는 프로세스 내에서 실행 흐름의 단위로, 여러 스레드가 병렬적으로 실행되며 작업을 분담할 수 있다. 그러나 스레드를 사용할 때는 레이스 컨디션(Race Condition)이나 데드락(Deadlock) 같은 문제가 발생할 수 있다.
(상점비순)
데드락은 스레드들이 서로 자원을 기다리며 영구적으로 대기하는 상황이다. 이는 네 가지 조건이 동시에 충족될 때 발생할 수 있다: 상호 배제, 점유 대기, 비선점, 순환 대기이다. 이 중 하나의 조건만 제거해도 데드락을 방지할 수 있다.
상호배제(Mutual Exclusion)
상호배제는 여러 스레드가 공유 자원을 동시에 접근할 때 발생할 수 있는 충돌을 방지하기 위한 메커니즘이다. 이를 통해 공유 자원이 한 번에 하나의 스레드만 접근할 수 있도록 제어한다. 이 메커니즘이 지켜지지 않으면 경쟁상태(Race Condition)가 발생할 수 있다.
경쟁 상태는은 여러 스레드가 동시에 공유 자원에 접근하면서 데이터가 손상되는 문제이다. 예를 들어, 하나의 변수에 대해 한 스레드가 ++ 연산을, 다른 스레드가 -- 연산을 동시에 실행하면, 결과가 예측과 다르게 나올 수 있다. 이는 연산이 원자적이지 않기 때문에 발생한다. 원자적이란 말은 우리가 컴파일러 상에서 볼 때는 ++을 하는 코드 한 줄이지만 low level에서는 값을 읽기 , 연산하기, 값을 쓰기와 같이 크게 세 단계에 걸쳐서 진행된다. 이 단계들이 한 번에 이루어지지 않고 여러 스레드에서 하나씩 일어난다면 오류가 일어날 수 있다. 말로 하면 복잡하니 예를 들어보자.
int counter = 0; // 공유 변수
void increment() {
for (int i = 0; i < 100; ++i) {
++counter; // 증가
}
}
void decrement() {
for (int i = 0; i < 100; ++i) {
--counter; // 감소
}
}
int main() {
std::thread t1(increment); // 스레드 1: 증가 작업
std::thread t2(decrement); // 스레드 2: 감소 작업
t1.join(); // 스레드 1 완료 대기
t2.join(); // 스레드 2 완료 대기
// 최종 결과 출력
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
이 코드에서 최종 결과는 실행 될 때마다 달라졌으며 우리가 원하는 0은 나오지 않았다.
Read | 값을 읽음 (counter = 0) | 값을 읽음 (counter = 0) | 두 스레드 모두 동일한 값을 읽음. |
Calculate | counter + 1 = 1 계산 | counter - 1 = -1 계산 | 서로 다른 결과 계산됨. |
Write | 값을 씀 (counter = 1) | 값을 씀 (counter = -1) | Thread 2가 Thread 1의 결과를 덮어씀. |
최종 결과 | - | - | counter = -1 (올바르지 않은 결과). |
위 표와 같이 Calculate 과정에서 동시에 연산이 되며 결과 값이 덮어 씌면서 계산 값이 달라진다.
이런 문제를 해결하기 위해 atomic키워드를 써서 변수를 정의하거나 뮤텍스(Mutex)와 세마포어(Semaphore) 같은 lockguard를 사용할 수 있다.
뮤텍스는 공유 자원을 사용할 때 다른 스레드가 접근하지 못하도록 잠금(lock)하고, 작업이 끝나면 잠금을 해제(unlock)하는 방식으로 동기화를 보장한다.
세마포어는 동시에 접근 가능한 스레드의 개수를 조정할 수 있는 카운팅 메커니즘으로, 자원이 제한된 상황에서도 유용하게 사용된다.
점유 대기 (Busy Waiting)
점유 대기란 한 스레드가 특정 조건을 만족할 때까지 CPU를 계속 점유하며 대기하는 상황을 의미한다. 이 과정에서 CPU는 조건 확인 작업에 사용되며, 다른 스레드나 프로세스가 실행될 기회를 잃게 된다. 점유 대기는 자원을 비효율적으로 사용하는 방식으로, 조건 충족이 느릴 경우 전체 시스템의 성능을 저하시킬 수 있다.
점유 대기는 조건이 빠르게 충족될 것으로 예상되는 프로그램에 적합하며, Spinlock을 활용하여 해결할 수 있다. Spinlock은 스레드가 잠금을 획득하기 위해 대기하는 동안 반복적으로 잠금 상태를 확인하는 기법이다. Spinlock은 짧은 시간 내에 조건이 충족될 때 효과적이지만, 장시간 대기에는 부적합하다. 이를 통해 점유 대기를 줄이고 시스템 자원을 효율적으로 사용할 수 있다.
비선점 (Non-Preemption)
비선점은 운영체제가 실행 중인 프로세스나 스레드에서 CPU를 강제로 빼앗지 않는 방식이다. 이는 프로세스가 스스로 작업을 종료하거나 I/O 요청 등으로 CPU를 자발적으로 반환할 때까지 계속 실행되도록 한다. 이 방식은 실행 중인 프로세스의 안정성을 보장하며, 콘텍스트 스위칭 오버헤드가 적다는 장점이 있다.
그러나 비선점 방식은 긴 작업이 CPU를 점유할 경우, 짧은 작업이 오랫동안 실행되지 못하는 기아 현상이 발생할 수 있다. 이를 해결하기 위해 우선순위 스케줄링을 활용하여 기아 상태를 방지한다. 우선순위 상속(Priority Inheritance)과 같은 기법을 통해 낮은 우선순위 작업이 중요한 작업에 영향을 미치지 않도록 조정할 수 있다.
순환 대기 (Circular Waiting)
순환 대기란 여러 스레드가 서로의 자원을 기다리며 순환적으로 의존 관계를 형성해 아무것도 실행되지 못하는 상태를 의미한다. 예를 들어, 스레드 A가 자원 X를 점유하고 자원 Y를 기다리는 동시에, 스레드 B는 자원 Y를 점유하고 자원 X를 기다리는 상황이 발생할 수 있다. 이러한 순환 관계는 교착 상태(Deadlock)를 야기한다.
순환 대기는 자원의 의존 관계가 명확하지 않아서 발생한다. 이를 해결하기 위해 자원 할당 순서를 정해 순환 의존 관계를 끊는 방법을 사용할 수 있다. 예를 들어, 모든 프로세스가 동일한 순서로 자원을 요청하도록 강제하거나, 자원 할당 시 우선순위를 정하는 방법이 있다. 이러한 방식으로 순환 대기를 방지하여 시스템의 안정성을 확보할 수 있다.
결론
프로세스와 스레드가 어디서 어떻게 쓰이는지, 사용 할 때 주의점은 어떤 것인지 알아보았다. 특히, 상용엔진에서 각각의 렌더링 파트마다 자동으로 멀티스레드가 실행되어 최적화를 한다. 우리는 이 방법이 왜 쓰이고, 쓰였을 때 왜 최적화가 되는지 알아보았다.
마무리
위의 내용을 바탕으로 개발자 면접간
1. 프로세스와 스레드의 차이는 무엇인가요?(이제 heap과 stack을 곁들여서)
2. 언리얼 엔진이나 유니티 엔진에서 어느 부분에 멀티스레드가 활용되나요?
3. 멀티스레드를 구현할 때 주의점이 무엇이 있나요?
4. 데드락이란 어떤 것인가요?
5.Race Condition이란 어떤 것인가요?
등에 대한 질문을 생각해 볼 수 있겠다. 스스로에게 질문을 해보고 답해보자.
헷갈리는 부분은 보완하기! 찡긋 O.<
틀린 부분이나 보완해줬으면 하는 내용이 있으면 댓글로 알려주세요! 추가 설명이 필요하신 부분도 댓글로 알려주세요.
어떠한 피드백도 환영입니다. 긴 글 읽어주셔서 감사합니다.