Python 이 Single Thread 에서 동작하는 이유에 대해 알아보던 중에 GIL(Global Interpreter Lock) 에 대해 알게 되었고, 이참에 Process 와 Thread 를 다시 정리해보면 좋을 것 같아 작성하게 된 글이다.
📌 Thread
Thread in computer science is the execution of running multiple tasks or programs at the same time. Each unit capable of executing code is called a thread.
Thread - MDN
스레드는 여러 작업이나 프로그램을 동시에 실행하는 실행 단위이며, 코드를 실행하는 각각의 단위이다.
프로세스는 최소한 하나의 메인 스레드를 가지고 있으며, 여러개로 스레드로 구성되어 병렬적으로 작업을 수행할 수 있다.
즉, 스레드는 프로세스 내부의 실행 단위이자, 흐름(Flow)이다.
프로세스가 CPU 자원을 할당받는 프로그램의 작업 단위였다면, 스레드는 각 프로세스의 실행 단위이며 하나의 프로세스는 여러개의 스레드로 구성될 수 있다.
따라서 스레드를 각 프로세스의 Subset 으로 볼 수 있으며, 프로세스와 비교할 때 여러가지 유사점 및 차이점을 가진다.
먼저, 스레드는 프로세스가 할당받은 자원을 공유하여 사용한다.
하나의 프로세스는 CPU로부터 Code, Data, Stack, Heap 메모리를 할당받게 되는데 이는 독립적인 공간이기 때문에 프로세스 간 통신(IPC)을 사용해야만 다른 프로세스의 자원에 접근할 수 있다.
하지만 스레드는 하나의 프로세스 내에서 Code, Data, Heap 메모리를 공유하여 사용하게 되며, Stack 메모리만을 독립적으로 사용하게 된다.
이는 기본적으로 하나의 프로세스 작업을 여러 스레드가 병렬적으로 수행하기 위해서는 실행 시 변경되지 않는 로직과 전역 변수들이 스레드 간 공유되어야 하기 때문이다.
즉, Code, Data, Heap 메모리에는 상수, 조건문, 반복문, 전역변수, 정적변수, 구조체 등 프로세스 실행 도중 지속적으로 참조되어야 하는 값들이 저장되기 때문에 스레드는 위와 같은 영역을 공유하여야 하는 것이다.
반면 Stack 의 경우, 함수가 종료되면 메모리에서 해제되는 영역이기 때문에 스레드 간 Stack 영역의 자원들을 공유할 필요가 없다.
또한, 스레드가 독립적인 Stack 영역을 할당받게되어 독립적인 함수 호출이 가능해지고, 이를 통해 각 스레드가 독립적인 실행 흐름을 수행할 수 있기 때문에 Stack 영역의 독립적 할당은 스레드의 독립적 실행을 위한 최소 조건인 셈이다.
뿐만 아니라, 스레드는 독립적인 PC Register 를 할당받는다.
이는 프로세스가 명령어 포인터를 통해 이전 작업 내용을 저장하고 있는 것과 같이, 스레드가 현재까지 수행한 명령어의 위치를 저장해 이전 작업 내용을 로드하여 연속적으로 수행될 수 있게 한다.
정리하면, 스레드는 프로세스가 할당받은 메모리 자원 중 Code, Data, Heap 영역을 공유하여 사용하며, Stack 영역을 독립적으로 할당받는다. 이는 스레드의 독립적 실행 흐름을 위한 최소 조건이다.
또한, 프로그램 카운터 레지스터(PC Register) 를 독립적으로 할당받아 이전 작업 내용을 로드하여 연속적인 작업이 가능해진다.
프로세스의 PCB가 OS 의 커널에 저장되어 있는 것처럼, 스레드 또한 각 스레드의 식별 및 제어 정보가 담긴 자료구조를 가지고 있다.
이는 Thread Control Block, TCB 라고 불리며 그 내용은 아래 사진과 같다.
프로세스의 일부 영역을 공유하고 있기 때문에 PCB 와 비교하여 단순한 구조를 가지고 있고, 그 내용 또한 유사한 것을 확인할 수 있다.
각 스레드를 식별하는 Thread ID, 스케줄링을 위한 상태값과 우선순위를 저장하고 있는 Thread State, Thread Priority, 이전 작업 내용의 위치를 저장하고 있는 PC Register, 심지어 프로세스와 유사하게 부모 스레드와 자식 스레드를 가리키는 포인터 정보까지 담고 있다.
이렇듯, TCB 는 PCB 와 유사하지만 비교적 가벼운 형태의 자료구조이며 프로세스가 생성될 때 함께 생성된다. 프로세스는 최소한 하나의 메인 스레드를 가지게 되기 때문이다.
또한, 프로세스는 여러개의 스레드로 구성될 수 있기 때문에 각각의 TCB 를 가리키는 주소는 리스트에 담기고, 리스트를 가리키는 메모리 주소가 최종적으로 PCB 에 저장되어 있다.
앞서 TCB 에 대해 알아보면서, Thread State 와 Thread Priority 항목을 발견할 수 있었다. 이는 프로세스와 마찬가지로, 스레드 또한 각각의 상태를 가지며 스레드 간 문맥교환(Context Switching)이 이루어진다는 사실을 의미한다.
즉, 프로세스 내의 다중 스레드 환경에서 각각의 스레드는 상태값과 우선순위를 가지게 되며 이를 통해 스케줄링되면서 병렬적으로 실행되기 때문에 필연적으로 프로세스처럼 문맥교환의 과정이 일어날 수 밖에 없는 것이다.
하지만 스레드의 문맥교환은 프로세스의 문맥교환보다 비교적 빠르고 가볍기 때문에 효율이 좋다.
프로세스의 문맥교환에서 현재 프로세스의 작업 내용과 PCB 정보를 저장하고, 새로운 프로세스의 작업 내용 및 PCB 정보들을 새롭게 로드하는 과정이 상당한 양의 메모리와 시간을 필요로 했다면,
스레드의 문맥교환에서는 프로세스의 공유 영역은 교체할 필요없이, PC Register 와 Stack 메모리만 교체해주면 되기 때문이다.
이렇듯, 멀티 스레드를 활용하면 문맥교환의 Overhead 를 최소화하고 속도와 메모리 측면에서 이점을 가질 수 있으며, 최종적으로 시스템의 처리율과 프로그램의 응답속도, 처리속도 향상을 기대할 수 있다.
따라서, 멀티 스레드를 사용하였을 때 얻을 수 있는 이점을 정리해보면 다음과 같다.
대부분 자원의 공유로 인한 시간 및 메모리 비용의 경제성과 효율성을 맥락으로 하고 있다.
물론, 멀티 스레드 환경에서 자원을 공유하면서 일어나는 다양한 문제점 또한 존재한다.
자원을 공유하는 멀티 프로세스 혹은 멀티 스레드 환경에서는 둘 이상의 프로세스, 스레드가 동일한 자원에 접근하고자 할 때 다양한 문제가 발생할 수 있다.
대표적으로 경쟁 조건(Race Condition), 교착 상태(Deadlock), 기아 상태(Starvation), 라이브락(Livelock) 의 문제가 있다.
Thread 에 대한 기본적인 개념을 Process와 비교하는 방식으로 정리하였다.
다음 포스팅은 Python 의 GIL(Global Interpreter Lock) 에 대해 정리해볼 계획이다.