본문 바로가기
IT 공부/운영체제

[OS] - 프로세스와 스레드의 차이 (매우 중요)

by exdus3156 2023. 12. 28.

1. 멀티프로세스가 필요한 경우 (vs 쓰레드)

먼저, 멀티프로세스와 멀티쓰레드간 차이에 대해 고심하는 것이 은근히 까다로운 이유는 옛날과 현재의 멀티태스킹 프로그래밍 관점이 다르기 때문에 발생한다.

예를 들어, 웹 서버를 구현해야 한다고 생각해보자. 웹 서버는 한 번에 여러 명의 사용자들에게 서비스를 제공해야 한다. OS가 TCP 프로토콜을 수행하고, 소켓을 연결하고, HTTP 데이터를 파싱하고 해석하고, 이 데이터를 다시 식별된 URI를 담당하는 웹 서버의 특정 로직에게 넘겨주고, ... 이런 일련의 동작을 하나의 사용자에게 할당해야 한다. 

옛날에는 이러한 요구를 한 번에 수행하기 위해 자식 프로세스를 생성하여 각각의 사용자의 요구를 처리할 수 있도록 프로그래밍했다. 물론 프로세스 생성 대신, 한 명의 사용자 요구를 하나씩 처리하며 순차적으로 처리할 수도 있지만, 당연하게도 이런 방식은 그다지 효율적이지도 않고 대기 상태의 사용자는 기다려야 한다는 생각에 복통이 터질 것이다.

이러한 예시가 시사하는 바는, 어떤 프로그램은 하나의 실행 흐름으로 프로그래밍하기에는 아쉬운 것들이 있다는 점이다. 사실 단일 실행 프로그램보다는 다양한 실행 흐름으로 논리가 분할되어 실행되는 프로그램이 훨씬 더 많고 어떤 면에서는 이런 프로그램이 직관적으로도 와 닿는다.

워드, 게임 같은 대부분의 프로그램이 다양한 실행흐름을 갖추는 것이 좋다. 워드의 경우 사용자의 입력을 받아 화면에 출력하는 부분과, 문서의 문법을 검사하는 프로그램은 서로 개별적인 실행 흐름을 두는 것이 좋다. 게임도 마찬가지로 사용자의 입력을 받아 로직을 처리하는 부분과 화면에 출력하는 부분이 따로 설계되는 것이 개발자가 생각하기에도 좋다.

옛날에는 이런 것들을 모두 프로세스를 따로 설계하는 식으로 달성했다. 즉, 완전히 개별적인 프로그램으로 만들었던 것인데 이것은 다양한 면에서 단점이 많은 방식이다. 컨텍스트 스위칭(context switching)의 비용도 크고, 프로세스는 기본적으로 OS가 관리하므로 생성 작업도 부담스럽다.

특히, 프로세스를 구분해봤자 결국 하나의 프로그램 로직 안에 있는 개별적인 실행 흐름이므로, 언젠가는 프로세스 간 데이터를 공유해야 한다. (할 필요가 없다면 애초에 다른 프로그램일 것이다!) 즉, 프로세스 간 통신이 원활해야 하는데, IPC는 커널을 통해 통신이 가능하므로 비용이 꽤 만만치 않은 편이다.

그래서 쓰레드(thread) 개념이 탄생했고, 이는 프로그램에서 개별적인 실행 흐름을 분할하면서도 자원을 일부 공유하는 프로그램을 하기 위해 사용된다.

스레드 개념이 탄생하면서 프로세스를 바라보는 기준이 달라졌다. 이제 멀티프로세스는 개별적인 프로그램을 다루는데 적합한 개념이 되었다.

결론적으로, 멀티프로세스에서 말하는 프로세스는 서로 완전히 다른 프로그램을 다루는데 적합한 개념이다. 예를 들어, 유튜브로 영상을 보는 것과, 밀리의 서재(ebook)로 책을 보는 것은 서로 완전히 다른 프로그램이므로 개별적인 프로세스로 취급해야 할 것이다.

 

2. 쓰레드와 프로세스의 차이 (메모리 구조)

스레드는 하나의 프로그램에서 여러 개의 실행 흐름을 분할하기 위해 사용한다. 서로 완전히 다른 프로그램이라면 프로세스를 생성하는 것이 맞다. 그러나 하나의 프로그램이면서 서로 공유하는 부분(데이터와 코드)가 있다면 스레드가 맞다.

메모리 구조의 차이를 보면 왜 스레드를 분할하는 것이 프로세스 분할과 IPC로 프로그램을 설계하는 것보다 더 나은지 알 수 있다.

프로세스는 메모리 구조 상 완전히 개별적으로 취급되는 반면, 스레드는 위와 같이 전역데이터, 힙, 코드 부분을 공유한다. 

즉, 커널을 통해 스레드를 생성하면 프로세스가 fork() 마냥 완전히 분리되는 것이 아니라 해당 스레드의 스택만 개별적으로 할당되고 나머지는 그대로다. 

스택이 독립이기 때문에 각자 스레드마다 자신만의 실행 흐름을 가질 수 있다. 

동시에 전역데이터 및 힙을 공유하기 때문에, 해당 자원으로의 모든 스레드의 접근이 가능하다. 이것은 개별 프로세스에서는 불가능한 일이다. 프로세스는 서로의 자원에 접근할 수 없다. 그것은 오로지 커널이 제공해주는 IPC(프로세스 간 통신) 서비스를 통해서만 가능하며 이 또한 메모리 내에 독자적인 공유 메모리 공간을 마련해야 한다. 그러나 스레드는 그대로 힙이나 데이터 공간에 접근하면 된다.

또한 코드 영역을 공유한다. 스택은 어디까지나 코드를 실행하면서 필요로 하는 작업 데이터를 저장하는 공간이다. 작업 데이터를 따로 개별적으로 두기 때문에 서로 실행 흐름을 독립적으로 관리할 수 있으나, 코드는 공유하므로 각 스레드는 분리되어 있더라도, 로직 자체는 동일한 로직을 처리할 수도 있다.

이것은 프로세스에게는 불가능한 일이다. 사실 IPC로 데이터를 공유하는 것보다도 더 말이 안 되는 일이다. 프로세스가 다른 프로세스의 코드에 접근해 로직을 빌려올 수는 없다.

코드 공유와 관련해서는 아래의 그림이 도움이 될 것이다.

코드 부분만을 강조한 그림을 그려보았다.

프로세스가 시작되어 main이 호출되면 해당 코드의 로직이 실행되고, 필요한 데이터는 스택 프레임에 적재될 것이다. 그러다 커널을 통해 스레드 생성을 호출하면 원래 있던 로직 중 해당 부분(그림에서는 main of thread)을 호출하고 필요한 스택을 새로 독립적으로 할당해준다.

재밌는 점은 프로세스의 main과 스레드의 main들은 func1, func2, func3 코드를 공유할 수 있다는 점이다. 프로세스가 최초로 호출되면 가장 먼저 프로세스의 main이 실행되며 스레드를 호출하면서 독자적인 실행 흐름이 분기된다. 그러나 코드를 공유하기 때문에 프로세스와는 달리 서로 같은 로직을 호출할 수 있게 된다.

(※ 설명을 위해 main 이라고 이름을 붙였지만 실제로 스레드를 main이라는 이름으로 호출하는 것은 권장되지 않는다.)

※ 프로세스와 스레드 간 스위칭 부분은 이전에 정리했던 포스트로 대체... (링크)

 

3. 주의할 점 몇 가지..

일단, 프로세스와 쓰레드가 구분되는 개념이긴 하지만 사용하는 운영체제에 따라, 그리고 같은 운영체제라도 버전에 따라 구현에 있어서 차이가 날 수 있다는 점이다. 실질적으로 플랫폼이 어떻게 프로세스와 스레드를 운영하는지 유연하고 기민하게 생각할 필요가 있다.

그리고 모든 프로그램은 근본적으로는 프로세스다. 스레드는 그 프로세스 내부에서의 실행 흐름을 나타낼 뿐이다. 멀티스레드 프로그래밍이 가능하다고 해도, OS에 의해 처음 소프트웨어가 시작될 때는 프로세스로서 할당되며, 프로세스로서 main 함수 스레드가 시작될 것이다. 그 main 함수가 내부적으로 커널 쓰레드 생성을 호출하면서 비로소 멀티쓰레딩이 시작되는 것이다. 이 원리는 스레드 자체의 제약 조건과도 연관이 있다. (링크)