지난 포스트에서 프로세스에 대해서 알아보았습니다. 프로세스의 특징은 독립적인 메모리 공간을 가지고 있어서 멀티 프로세스 환경에서는 서로 영향을 미치지 않아서 안정성이 높은 반면 새로운 프로세스를 생성하는 것과 프로세스의 문맥전환 과정에서 많은 오버헤드가 발생한다는 특징이 있습니다. 이와 같은 문제를 해결하고자 프로세스보다 작은 단위의 실행 수단을 프로세스 내에서 만들어 한 프로세스에서 각각 별도의 진행 플로우를 만들자는 아이디어가 나왔고 그의 결과물이 스레드의 등장입니다.
스레드는 프로세스 내에서 실행되는 흐름의 단위를 의미합니다. 프로세스 내에는 적어도 한개의 스레드가 존재하며 한 프로세스 내 여러 쓰레드는 메모리를 공유해 나가며 작업을 수행합니다. 간단하게 예를 들면 유튜브에 여러 요청이 발생했을 때를 보겠습니다.
여러 사용자가 동영상 재생 요청을 보내게 되면 유튜브 내부에서는 여러 쓰레드가 독자적으로 각 요청에 대한 처리를 진행합니다.
스레드는 프로세스 내에서 스택 영역만 할당 받고 힙, 데이터, 코드 영역을 서로 공유해서 이용합니다. 힙영역을 공유해서 사용하기 있기에 스레드간의 협력은 프로세스간의 협력에 비해 비교적 간단하게 진행됩니다. 하지만 힙 영역의 데이터를 공유해서 이용하다보니 여러 스레드가 동시에 동작하는 과정에서 race condition 문제와 데드락이 발생할 수 있습니다.
💡 왜 스택 영역만 독자적으로 가지는 것일까?
스택 영역은 함수 호출 시 매개 변수, 지역 변수가 저장되는 영역입니다. 해당 영역을 각각 가진다는 것은 스레드마다 독립적으로 함수 호출이 가능하다는 것을 의미하고 이는 여러 스레드가 같이 동작하는 것이 아닌 독립적인 실행 흐름을 가질 수 있다는 것을 의미합니다.
스레드는 프로세스 내에서 생성 및 동작하기 있기에 프로세스 제어블록 내에서 스레드 제어블록을 참조해 운영체제가 스레드 제어블록을 관리할 수 있게 합니다.
요소 | 설명 |
---|---|
스레드 ID (TID) | 각 스레드를 구분하기 위한 고유한 식별자. 프로세스 내에서 각 스레드를 식별하는 데 사용됩니다. |
상태 (State) | 스레드의 현재 상태를 나타냅니다. 예: Running, Ready, Blocked 등. |
프로그램 카운터 (PC) | 현재 실행 중인 명령어의 주소를 저장합니다. 스레드가 중단되었다가 재개될 때 실행 위치를 추적합니다. |
스택 포인터 (SP) | 스레드의 스택이 위치하는 메모리 주소를 가리킵니다. 각 스레드는 독립적인 스택을 가집니다. |
레지스터 값 | 스레드가 실행 중일 때 사용하는 CPU 레지스터의 값을 저장합니다. 각 스레드마다 독립적인 레지스터 세트가 필요합니다. |
우선순위 (Priority) | 스레드의 실행 우선순위를 나타냅니다. 스케줄링을 할 때 우선순위에 따라 실행 순서가 결정됩니다. |
소속 프로세스 정보 | 해당 스레드가 속한 프로세스에 대한 정보입니다. 보통 PCB(프로세스 제어 블록)를 참조합니다. |
스레드는 크게 사용자 스레드(User Level Thread)
와 커널 스레드(Kernel Level Thread)
로 나눌 수 있습니다.
사용자 스레드
는 프로그래밍 언어 내에서 라이브러리를 통해 구현된 스레드를 의미합니다. 커널의 지원 없이 사용자 영역에서 스레드의 생성과 관리가 이루어집니다. 커널은 해당 프로세스만 인식할 뿐 스레드를 인식하지 못하며 그로인해 스레드간 문맥 교환과정에서 커널 모드의 전환이 필요없기에 성능상의 이점이 있습니다. 하지만 해당 프로세스가 특정 상황(I/O 등)등으로 인해서 block이 되면 내부 모든 스레드 작업들이 모두 멈추는 문제가 발생합니다.
반면 커널 스레드
는 운영체제가 직접 관리하며, 커널이 스케줄링의 대상으로 인식하고 관리합니다. 이를 통해 스레드 단위로 작업을 스케쥴링할 수 있다는 점과 멀티 프로세서에서 여러 스레드를 병렬로 처리가 가능합니다. 하지만 스레드간의 스케쥴링을 통해서 문맥교환이 발생하고 이로인해 오버헤드가 발생할 수 있습니다.
CPU에서는 커널 스레드가 동작되기에 CPU에 사용자 스레드가 할당되고 스케줄링이 되기 위해서는 사용자 스레드와 커널 스레드가 반드시 연결이 되어야 합니다. 사용자 스레드와 커널 스레드의 연결 방식에 대해서 알아보겠습니다.
✏️ one-to-one model
✏️ many-to-one model
✏️ many-to-many model
이번에는 멀티 프로세스와 멀티 쓰레드의 특징을 비교해보도록 하겠습니다. 두 방식 모두 병렬 처리를 위한 방법이지만, 각각의 장단점이 있어 상황에 따라 적절한 선택이 필요합니다.
비교 기준 | 멀티 프로세스 | 멀티 쓰레드 |
---|---|---|
메모리 사용 | 각 프로세스마다 독립적인 메모리 영역 필요 | 스레드간 메모리 공유로 효율적 |
통신 방식 | IPC를 통한 통신 필요 | 공유 메모리를 통한 간단한 통신 |
안정성 | 하나의 프로세스가 죽어도 다른 프로세스에 영향 없음 | 하나의 스레드 문제가 전체 프로세스에 영향 |
멀티 프로세스와 멀티 스레드의 차이를 살펴보면, 멀티 프로세스는 안정성이 높지만 메모리 사용량이 많고 프로세스 간 통신이 복잡한 반면, 멀티 스레드는 메모리를 효율적으로 사용하고 통신이 간단하지만 안정성이 상대적으로 낮다는 특징이 있습니다. 따라서 시스템의 요구사항과 특성에 따라 적절한 방식을 선택하는 것이 중요합니다.
이러한 특성들을 고려할 때, 웹 서버와 같이 많은 클라이언트 요청을 처리해야 하는 서비스에서는 멀티 스레드 방식이 자주 사용됩니다. 반면, 하나의 작업이 실패해도 다른 작업에 영향을 주지 않아야 하는 중요한 시스템에서는 멀티 프로세스 방식이 선호됩니다. 실제 많은 현대 시스템들은 두 방식을 혼합하여 사용함으로써 각각의 장점을 최대한 활용하고 있습니다.
멀티 쓰레드를 사용할 때는 여러 가지 주의해야 할 점들이 있습니다. 가장 중요한 것은 여러 스레드가 공유 자원에 동시에 접근할 때 발생할 수 있는 동기화 문제입니다. 이러한 문제를 해결하기 위해 뮤텍스(Mutex)나 세마포어(Semaphore)와 같은 동기화 메커니즘을 적절히 사용해야 합니다.
또한 데드락(Deadlock) 상황을 방지하기 위해 자원 할당 순서를 일관되게 유지하고, 적절한 타임아웃을 설정하는 것이 중요합니다. 스레드 풀(Thread Pool)을 사용하여 스레드의 생성과 소멸에 따른 오버헤드를 줄이는 것도 효율적인 멀티스레드 프로그래밍을 위한 좋은 방법입니다.