프로세스 & 스레드란?
프로세스 (Process) | 스레드 (Thread) |
운영체제로부터 자원을 할당받은 작업의 단위 | 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위 |
프로세스의 작업의 단위라는 단어와 스레드의 실행 흐름의 단위라는 단어가 이해가 잘 가지 않을 수 있다.
웹 브라우저로 예시를 들어보겠다. 웹 브라우저가 하나의 프로세스라고 생각했을 때, 웹 브라우저는 여러 웹 페이지를 불러오는 작업을 처리할 수 있다. 이 때, 여러 탭을 열어 놓는 것을 여러 스레드가 동시에 작업을 처리하는 것으로 볼 수 있다. 각 탭은 독립적으로 웹 페이지를 불러오지만, 모두 같은 웹 브라우저의 자원을 공유한다. 즉, 한 탭(스레드) 에서의 작업이 다른 탭의 작업에 직접적으로 영향을 주지는 않지만, 모두 같은 프로세스 내에서 실행되므로 자원을 공유하고 있다.
프로그램과 프로세스
- 정적 프로그램(Static Program)
프로그램은 윈도우의 *.exe 파일이나 Mac의 *.dmg 파일과 같은 컴퓨터에서 실행할 수 있는 파일을 칭한다.
그러나 아직 파일을 실행하지 않은 상태이기 때문에 정적 프로그램(Static Program) 줄여서 프로그램(Program)이라고 부른다.
어떠한 프로그램을 개발할 땐 자바나 C언어와 같은 언어를 이용해 코드를 작성하여 완성된다. 즉, 프로그램은 쉽게 말하면 그냥 코드 덩어리인 것이다.
- 프로세스(Process)
프로그램이 코드 덩어리라면, 프로세스는 프로그램을 실행시켜 정적인 프로그램이 동적으로 변하여 프로그램이 돌아가고 있는 상태를 말한다. 즉, 컴퓨터에서 작업 중인 프로그램을 의미하는 것이다.
모든 프로그램은 실행되기 위한 메모리 공간을 운영체제가 할당해 주어야 실행될 수 있다. 따라서 프로그램을 실행하는 순간 파일은 컴퓨터 메모리에 올라가게 되고, 운영체제로부터 프로그램 실행을 위해 필요한 메모리 공간 및 시스템 자원(CPU)을 할당받아 프로그램 코드를 실행시켜 우리가 서비스를 이용할 수 있게 되는 것이다.
즉 프로그램과 프로세스의 차이는 똑같은 어플리케이션을 실행 하냐 안하냐일 뿐이다.
프로그램 | 프로세스 |
어떤 작업을 하기 위해 실행할 수 있는 파일 | 실행되어 작업 중인 컴퓨터 프로그램 |
파일이 저장 장치에 있지만 메모리에는 올라가 있지 않은 정적 상태 | 메모리에 적재되고 CPU 자원을 할당받아 프로그램이 실행되고 있는 상태 |
코드 덩어리 | 코드 덩어리를 실행한 것 |
스레드
- 프로세스의 한계
예를 들어, 한 음식점(프로세스)이 한 번에 하나의 주문(작업)만 처리할 수 있다고 한다. 만약 여러 손님이 동시에 주문(작업)을 했다면, 한 주문이 완전히 처리되고 나서야 다음 주문을 시작할 수 있다. 이는 매우 비효율적이고, 손님들은 자신의 주문(작업)이 처리될 때까지 오랜 시간을 기다려야 할 것이다.
이 문제를 해결하기 위해, 각 손님마다 별도의 음식점(프로세스)를 열어준다고 생각해보자. 이 방법은 각 손님의 주문(작업)을 동시에 처리할 수 있게 하지만, 각 음식점(프로세스)이 요리사, 주방 도구, 공간 등을 별도로 갖춰야 하므로, 매우 많은 자원을 소모하게 된다. 컴퓨터 관점에서 보면, 이는 메모리와 CPU 자원을 과도하게 사용하는 것과 같다.
- 스레드
스레드란, 하나의 프로세스 내에서 동시에 진행되는 작업 갈래, 흐름의 단위를 말한다.
예를 들어, 크롬 브라우저가 실행되면 프로세스 하나가 생성될 것이다. 그런데 우리는 브라우저에서 파일을 다운 받으며 온라인 쇼핑을 하면서 게임을 하기도 한다.
즉, 하나의 프로세스 안에서 여러가지 작업들 흐름이 동시에 진행되기 때문에 가능한 것인데, 이러한 일련의 작업 흐름들을 스레드라고 하며 여러 개가 있다면 이를 멀티(다중) 스레드라고 부른다.
그림에서 보이듯이 하나의 프로세스 안에 여러 개의 스레드들이 들어 있다고 보면 된다. 스레드 수가 많을 수록 이론적으로는 동시에 수행할 수 있는 작업의 수가 늘어나 프로그램의 성능이 향상될 수 있다.
🤯 스레드는 다다익선?
스레드 수가 많을수록 프로그램의 성능이 항상 올라가는 것은 아니다. 성능은 시스템의 자원, 작업의 종류, 그리고 스레드 간의 조정 방법 등 여러 요소에 의해 영향을 받는다. 예를 들어, 4개의 CPU 코어가 있는 시스템에서는 동시에 최대 4개의 스레드가 실제로 작업을 수행할 수 있다. 스레드 수가 CPU 코어 수를 초과하면, 스레드 간에 자원 경쟁이 발생하고 컨텍스트 스위칭으로 인한 오버헤드가 증가하여 성능이 저하될 수 있다.
일반적으로 하나의 프로그램은 하나 이상의 프로세스를 가지고 있고, 하나의 프로세스는 반드시 하나 이상의 스레드를 갖는다. 즉, 프로세스를 생성하면 기본적으로 하나의 main 스레드가 생성되게 된다. 스레드 2개, 3개 ... 는 프로그램을 개발한 개발자가 직접 프로그래밍하여 위치시켜주어야 한다.
프로세스 & 스레드의 메모리
지금까지 프로세스와 스레드의 단순 차이점에 대해 살펴보았다. 이제부턴 프로세스와 스레드의 내부 작동 방식을 이해하기 위해 설명을 작성해 보겠다. 운영체제가 시스템 자원을 어떤 방식으로 할당하고 프로그램은 자원을 어떤 방식으로 활용하는지 내부 작동 방식을 이해해야 나중에 스레드 프로그래밍을 할 때 응용이 가능하다.
프로세스 자원 구조
프로그램이 실행되어 프로세스가 만들어지면 다음 4가지의 메모리 영역으로 구성되어 할당 받게 된다.
1. 코드 영역(Code/Text): 코드 영역엔 프로그래머가 작성한 프로그램의 실제 명령어들이 저장된다. 이 명령어들은 CPU가 이해하고 실행할 수 있는 기계어 형태로 변환되어 저장된다. 예를 들어, 프로그래머가 작성한 모든 함수와 명령이 이 영역에 위치한다.
2. 데이터 영역(Data): 프로그램이 실행되면서 사용되는 데이터가 저장되는 곳이다. 이 영역은 세부적으로 나누어진다.
- .data: 초기화된 전역 변수와 정적(static)변수가 저장된다. 예를 들어, 'int global_var = 5; ' 와 같은 변수가 여기에 해당된다.
- .BSS: 초기값을 가지지 않은 전역 변수와 정적 변수가 저장된다. 이 변수들은 프로그램 시작 시 0 또는 NULL로 초기화된다.
- .rodata: 읽기 전용 데이터, 예를 들어 상수 문자열이나 'const' 로 선언된 변수들이 저장된다.
3. 힙 영역(Heap): 동적으로 할당된 메모리가 저장되는 곳이다. 예를 들어, C언어에서 'malloc' 함수나 C++에서 'new' 연산자를 사용하여 메모리를 할당받은 겨우, 해당 메모리는 힙 영역에 위치하게 된다. 사용자가 직접 메모리를 할당하고 해제할 수 있기 때문에, 스택에 비해 더 유연하지만 관리가 더 복잡하다.
4. 스택 영역(Stack): 함수 호출 시 생성되는 지역 변수와 함수의 매개변수가 저장되는 곳이다. 스택은 LIFO(Last In, First Out) 구조를 가지며, 함수 호출이 종료되면 해당 함수에 할당된 스택 메모리가 자동으로 해제된다. 만약 이 영역이 넘치게 되면 Stack Overflow 오류가 발생한다.
코드 영역과 데이터 영역은 프로그램의 시작과 함께 크기가 결정되는 정적 영역이다. 반면, 스택 영역과 힙 영역은 프로그램 실행 동안 필요에 따라 크기가 변하는 동적 영역이다. 이 두 영역은 메모리에서 서로 반대 방향으로 성장한다. 스택은 메모리의 위쪽으로 성장하고, 힙은 아래쪽으로 성장한다.
- 스레드의 자원 공유
스레드는 프로세스가 할당 받은 자원을 이용하는 실행의 단위로써, 스레드가 여러개 있으면 우리가 파일을 다운받으며 동시에 웹 서핑을 할 수 있게 해준다. 스레드끼리 프로세스의 자원을 공유하면서 프로세스 실행 흐름의 일부가 되기 때문에 동시 작업이 가능한 것이다. 따라서 아래 사진처럼 하나의 프로세스 내에 여러 개의 스레드가 들어있는 상태인 것이다.
이 때 프로세스의 4가지 메모리 영역(Code, Data, Heap, Stack) 중 스레드는 Stack만 할당받고 Code, Data, Heap은 프로세스 내 다른 스레드들과 공유된다. 이는 스레드가 자신만의 스택 영역을 가진다는 것과 같다. 각 스레드에는 그 스레드가 실행하는 함수의 지역 변수, 매개변수 등이 저장된다. 따라서 각 스레드는 독립적으로 함수를 호출하고, 그 결과를 처리할 수 있다.
프로세스는 메모리의 코드, 데이터, 힙 영역을 공유하는 여러 스레드를 포함할 수 있다. 이는 스레드들이 프로그램의 기본 구조, 전역 변수, 동적 할당된 메모리 등을 서로 공유할 수 있음을 의미한다. 또한 각 스레드는 자신만의 스택 영역을 가지고 있다. 이는 각 스레드가 독립적으로 함수 호출과 반환, 지역 변수 관리 등을 수행할 수 있음을 의미한다. 독립적인 함수 호출이 가능하다는 것은 독립적인 실행 흐름이 추가된다는 것이다. 즉, stack을 가짐으로써 스레드는 독립적인 실행 흐름을 가질 수 있게 되는 것이다.
💡 반면 프로세스는 프로세스끼리 다른 프로세스의 메모리에 직접 접근할 수는 없다.
이러한 구조 덕분에, 스레드들은 효율적으로 협력하면서도 각자의 독립적인 작업을 수행할 수 있다. 프로세스를 다수의 실행 단위인 스레드로 구분하여 자원을 공유하고, 자원의 생성과 관리의 중복성을 최소화하여 수행 능력을 올렸기 때문이다.
- 프로세스의 자원 공유
기본적으로 프로세스는 메모리의 별도의 주소 공간에서 실행되기 때문에, 한 프로세스는 다른 프로세스의 변수나 자료구조에 접근할 수는 없다. 그러나 다른 프로세스 정보에 무조건 접근할 수 없는 것은 아니다.
우리가 사용하는 대부분의 컴퓨터 프로그램을 보면 다른 프로그램에 있는 정보를 가져오는 경우를 볼 수 있다. 이처럼 특별한 방법을 통해 프로세스가 다른 프로세스의 정보에 접근하는 것이 가능하다. 프로세스 간 정보를 공유하는 방법은 다음과 같다.
- IPC(Inter-Process Communication) 사용
IPC는 서로 다른 프로세스가 정보를 공유하거나 데이터를 교환하기 위해 사용하는 메커니즘의 총칭이다. IPC는 파이프, 메시지 큐, 공유 메모리, 소켓 등 다양한 형태로 구현될 수 있으며, 이는 프로세스가 서로 독립적으로 실행되면서도 필요한 정보를 교환할 수 있게 한다. IPC는 동일한 시스템 내의 프로세스 간 통신뿐만 아니라, 네트워크를 통한 원격 프로세스 간 통신에도 사용될 수 있다.
IPC에는 전통적으로 두 가지 모델이 있다. (a)Shared memory와 (b)Message Passing이다.
(a)의 경우, 협력 프로세스 간 공유되는 메모리를 생성 후 이를 이용하여 정보를 교환한다. 데이터의 형식과 위치는 프로세스들에 의해 결정되며 운영체제 소관은 아니다.(커널의 도움이 거의 필요없다.) 성능은 좋지만 프로세스 간 동기화 문제가 발생하여 애플리케이션에서 직접 동기화를 해야 한다.
(b)의 경우, 운영체제가 프로세스 간 통신 방법을 제공하고, 메세지를 대리 전달해준다. 운영체제가 동기화를 해주기 때문에 안전하고 동기화 문제는 없으나, 시스템 콜을 사용하기 때문에 성능이 떨어진다.
💡 LPC(Local Inter-Process Communication)은 IPC의 한 형태로, 특히 동일한 시스템 내에서 실행 중인 프로세스 간의 통신에 초점을 맞춘 메커니즘이다. 즉, 로컬 프로세스 간 통신 방식으로 message passing 또는 shared memory 모델을 사용할 수 있으며, 공유 메모리는 특히 로컬 프로세스 간 대량 데이터 공유에 유리하다.
원격 프로세스 간 통신은 주로 message passing 모델을 사용한다. 공유 메모리는 같은 시스템 내의 프로세스 간에 적합하며, 네트워크를 통한 원격 통신에서는 직접 사용되지 않는다.
그러나 프로세스 간 자원 공유가 가능하다고 해서, 프로세스 전환(Context Switch)을 스레드 대신 사용하기엔 부담이 크다. 프로세스 전환 시, 운영체제는 현재 실행 중인 프로세스의 CPU 레지스터 상태(프로그램 카운터, 스택 포인터 등)을 저장해야 하며, 다음에 실행할 프로세스의 레지스터 상태를 복원한다. 즉 CPU 레지스터 교체가 일어나야 하며, CPU는 데이터와 명령어의 빠른 접근을 위해 캐시 메모리를 사용하는데 프로세스 전환 시, 이전 프로세스의 작업과 관련된 데이터가 캐시에 남아 있을 수 있어 경우에 따라 캐시를 초기화해야 할 수도 있다. 또한 프로세스마다 사용하는 메모리 영역이 다르기 때문에, 프로세스 전환은 메모리 관리에도 영향을 미친다.
이러한 이유들로 프로세스 전환은 시스템 리소스에 상당한 부담을 줄 수 있으며, 특히 멀티태스킹 환경에서 프로세스 전환이 빈번히 일어날 때 시스템의 성능 저하를 일으킬 수 있다. 따라서 현대의 운영 체제와 응용 프로그램은 가능한 한 프로세스의 전환을 최소화하고, 다중 스레딩을 기본으로 하고 있다.
프로세스 & 스레드 동시 실행 원리
우리가 음악을 들으면서, 웹서핑을 하고, 메신저의 메시지를 확인할 수 있는 이유는 컴퓨터 내부적으로 프로세스와 스레드를 동시에 처리하는 멀티 태스킹(multi tasking) 기술 때문이다. 하지만 여기서 동시에 처리한다는 것이 단순히 CPU 프로세서가 프로그램들을 한꺼번에 동시에 실행하는 것으로 생각하겠지만, 내부적으로 복잡한 원리에 의해 처리된다. 그 원리는 운영체제 핵심 원리이기도 하다.
멀티 코어와 스레드
최근 노트북을 구매하려고 견적을 맞춰보는데, 이런 단어가 적혀있다.
먼저, CPU는 컴퓨터의 두뇌라고 할 수 있다. 이것은 컴퓨터가 수행해야 하는 모든 계산과 명령을 처리한다. 오늘날 대부분의 CPU는 여러 개의 코어(명령어를 메모리에서 뽑아 해석하고 실행하는 반도체 유닛)를 가지고 있는데, 각 코어는 CPU 내에서 독립적으로 명령을 처리할 수 있는 작은 처리기이다. 예를 들어, 4코어 CPU는 네 개의 작업을 동시에 처리할 수 있다.
스레드는 프로세스 내에서 실행되는 명령어의 흐름이다. 기본적으로, 각 CPU코어는 한 번에 하나의 스레드만 처리할 수 있다. 그러나 하이퍼스레딩(Hyper-Threading)기술을 사용하면, 각 코어가 두 개의 스레드를 동시에 처리할 수 있게 된다. 이 기술은 하나의 물리적 코어가 두 개의 논리적 코어(또는 스레드)로 동작할 수 있게 해준다.
예를 들어 4코어 CPU가 하이퍼스레딩을 지원한다면, 이 CPU는 8개의 스레드를 동시에 처리할 수 있다. 여기서 4코어는 물리적 코어 수를 말하고, 8스레드는 CPU가 동시에 처리할 수 있는 논리적 작업의 수를 의미한다.
결국, 하이퍼스레딩은 운영체제가 더 많은 작업을 동시에 처리할 수 있게 해준다. 이는 멀티태스킹 환경에서 특히 유용하다. 즉 컴퓨터가 여러 프로그램을 동시에 실행할 때 성능이 향상될 수 있다.
💡 여기서 CPU의 스레드는 우리가 배운 프로세스의 스레드와는 조금 다른 개념이다. CPU의 스레드는 하드웨어적 스레드이고 프로그램의 스레드는 소프트웨어적 스레드로 구분한다.
하드웨어 스레드(CPU의 스레드)는 CPU내의 물리적 또는 논리적 코어가 동시에 처리할 수 있는 작업의 흐름이다.
소프트웨어 스레드(프로그램의 스레드)는 프로그램 내에서 생성되고 관리되는 실행의 단위이다.
즉, 소프트웨어 스레드(프로그램이 생성한 스레드)가 최종적으로 CPU의 하드웨어 스레드(실제로 명령을 실행하는 CPU의 능력)에 의해 실행된다는 것이다. 소프트웨어적으로 생성된 스레드는 하드웨어적 지원이 있는 CPU 스레드에 할당되어 실행된다.
그런데 우리는 컴퓨터를 이용할 때, 프로그램을 수십, 수백개를 켜놓고 이용한다. 그럼 그 많은 프로세스들은 고작 8개의 논리적인 스레드로 어떻게 처리되는 것일까?
이 원리를 알기 위해선 병렬성(Parallelism)과 동시성(Concurrency)이라는 개념을 알고 있어야 한다. 이 개념은 운영체제의 프로세스, 스레드를 이해하는데 있어 가장 핵심이 되는 것들이다.
CPU의 작업 처리 방식
- 동시성(Concurrency)
동시성은 말 그대로 동시에 작업을 수행하는 것을 의미한다. 예를 들어 하나의 직원(싱글 코어)이 여러 가지 다른 작업(스레드)을 번갈아 가며 처리하는 것과 비슷하다. 이 직원은 한번에 하나의 작업만 할 수 있지만, 작업 사이를 빠르게 전환하면서 여러 작업을 처리한다. 이러한 방식은 작업이 동시에 실행되는 것처럼 보이게 하지만, 실제로는 한 작업을 잠시 중단하고 다른 작업으로 전환하는 방식으로 운영된다.
작업들을 번갈아가면서 실행할 때 작업들을 아주 잘게 나누어 아주 조금씩만 작업을 수행하고 다음 작업으로 넘어가는 식으로 동작되는데, 이렇게 진행 중인 작업들을 A->B 로 번갈아 바꾸는 것을 Context Switching이라고 부른다.
- 병렬성(Parallelism)
병렬성은 컴퓨팅에서 여러 작업을 동시에 실행하여 처리 속도를 향상시키는 개념이다. CPU의 병렬성은 이러한 개념을 하드웨어 수준에서 구현한 것으로, 특히 여러 개의 CPU 코어를 활용하여 다수의 작업을 동시에 처리하는 방식을 의미한다. 예를 들어 여러 직원(멀티 코어)이 각자 다른 작업을 동시에 처리하는 것과 비슷하다. 각 직원은 독립적으로 작업을 진행하기 때문에, 여러 작업을 동시에 완료할 수 있다.
동시성(Concurrency) | 병렬성(Parallelism) |
동시에 실행되는 것 같이 보이는 것 | 실제로 동시에 여러 작업이 처리되는 것 |
싱글 코어에서 멀티 스레드(Multi Thread)를 동작시키는 방식 | 멀티 코어에서 멀티 스레드(Multi Thread)를 동작시키는 방식 |
한 번에 많은 것을 처리 | 한 번에 많은 일을 처리 |
논리적인 개념 | 물리적인 개념 |
❓ 동시성은 왜 필요할까
동시성은 '동시에 실행되는 것처럼' 관리하는 데 초점을 맞춘다. 이는 실제로 작업들이 동시에 수행되지 않더라도, 여러 작업이 마치 동시에 진행되는 것처럼 보이게 하는 기법이다. 따라서 동시성 자체가 작업을 더 빠르게 마치게 하는 것은 아니다. 반면 병렬성은 정말로 각 코어에 프로세스를 나누어 실행하기 때문에 듀얼 코어면 시간이 반 이상 줄어들 것이다. 그렇다면 왜 이렇게 번거롭게 작업들을 스위칭하며 처리하는 것일까?
첫 번째는 하드웨어적 한계 때문이다. CPU 발열 때문에 성능을 올리기에는 한계에 봉착되었다. 따라서 코어의 성능을 올리는 대신 여러 코어를 탑재하여 쿼드 코어, 옥타 코어 CPU들을 출시하고 있다. 하지만 아무리 코어를 많이 넣어도 수십 개의 코어를 넣을 순 없으니 결국 하드웨어적 제한이 걸리게 되고, 수십 수백개의 프로세스를 돌리기 위해선 동시성이 필요한 것이다.
두 번째는 작업을 더 효율적으로 관리하기 위해서이다. 4코어 8스레드의 CPU의 환경에서 현재 총 16개의 작업이 있다고 가정해 보자. 그 중 8개는 오래 걸리는 작업이고, 나머지 8개는 짧은 시간을 필요로 하는 작업이라고 한다. 논리적인 8개 코어이니 최대 8개까지 동시에 실행할 수 있을텐데, 최악의 경우 8개의 오래 걸리는 작업이 먼저 동시에 처리되기 시작했다고 하자. 이 경우 나머지 가벼운 8개의 작업은 처리하는 데 짧은 시간이 걸리는 데에도 불구하고 현재 처리 중인 8개의 작업이 다 끝날 때까지 기다려야 할 것이다. 이러한 비효율을 극복하기 위해 작업을 아주 잘게 나눠 번갈아 가며 처리하는 동시성 개념을 채택한 것이다.
이처럼 동시성은 작업이 대기 시간을 가질 때 특히 유용하며, 이러한 대기 시간을 활용하여 다른 작업을 진행함으로써 시스템의 전반적인 처리량을 향상시킬 수 있다. 병렬성과 동시성은 각각 다른 상황과 요구사항에 따라 선택되어야 하며, 많은 소프트웨어적 스레드를 처리하기 위해선 적절히 섞어 사용되어야 한다.
프로세스 & 스레드의 생명 주기
프로세스와 스레드는 각각의 생명 주기를 가지고 있으며, 운영체제는 이러한 생명 주기를 관리하고, 프로세스와 스레드를 조정하여 시스템 자원을 효율적으로 사용할 수 있게 된다.
프로세스 스케쥴링
프로세스 스케쥴링(Process Schduling)은 운영체제에서 CPU를 사용할 수 있는 프로세스를 선택하고, CPU를 할당하는 작업을 말한다. 프로세스 스케쥴링은 프로세스의 우선순위, 작업량 등을 고려하여 효율적으로 배치하여, 이를 통해 운영체제는 CPU를 효율적으로 사용하며 시스템 전반적인 성능을 향상시킨다. 따라서 스케쥴링은 멀티 테스킹 작업을 만들어내는 데 있어서 핵심적인 부분이다.
스케쥴링은 운영체제의 특징과 시스템 요구사항에 따라 다양한 알고리즘 방식으로 동작된다. 알고리즘 종류로는 대표적으로 FCFS(First-Come, First-Served), SJF(Shortest-Job-First), RR(Round-Robin), 우선순위 기반(Priority Scheduling), 다단계 큐(Multilevel Queue) 등이 있다.
[비선점]
- FCFS(First-Come, First-Served)
FCFS, 즉 FIFO 알고리즘은 가장 간단한 스케쥴링 알고리즘으로, 먼저 도착한 프로세스를 먼저 처리한다. 프로세스는 도착 순서대로 큐에 배치되며, 실행 준비가 완료된 프로세스가 CPU를 차지한다. 구현이 간단하고 예측 가능하지만, 짧은 작업이 긴 작업 뒤에 도착할 경우, 긴 대기 시간(컨베이어 벨트 효과)이 발생할 수 있다. 이는 '기아 상태' 로 이어질 수 있다.
- SJF(Shortest-Job-First)
SJF는 실행 시간이 가장 짧은 프로세스를 우선적으로 스케쥴링한다. 이 알고리즘은 평균 대기시간을 최소화하는 데 효과적이다. 그러나 실행 시간이 긴 프로세스가 기아 상태에 빠질 수 있으며, 프로세스의 실행 시간을 사전에 정확히 알아야 한다는 점에서 실제 환경에서 구현이 어렵다.
- HRN(Highest Response Ratio Next)
각 프로세스의 대기 시간과 실행 시간을 고려하여 우선순위를 결정한다. 이 알고리즘은 '우선순위 스케줄링'의 변형으로 볼 수 있으며, 단순히 실행 시간이 짧은 작업을 우선적으로 처리하는 Shortest Job First(SJF) 알고리즘의 단점을 보완한다.
HRN 스케줄링에서는 각 프로세스의 응답률을 계산하여 가장 높은 응답률을 가진 프로세스를 다음에 실행한다.
- 대기시간: 프로세스가 준비 큐에서 실행을 기다린 시간
- 서비스 시간: 프로세스의 실행에 필요한 시간(실행 시간)
이 공식을 통해 대기 시간이 긴 프로세스와 실행 시간이 짧은 프로세스 모두 높은 우선순위를 받게 된다. 이는 실행 시간이 매우 짧지만 대기 시간이 길어진 길어진 프로세스가 기아 상태에 빠지는 것을 방지하고, 공정한 CPU 사용 시간 배분을 목표로 한다. 그러나 각 프로세스의 응답률을 계산하고, 주기적으로 우선순위를 업데이트 해야 하므로, 계산 복잡성이 높다. 또한 프로세스 서비스 시간을 사전에 알아야 하며 실제 시스템에서 정확히 예측이 어려울 수 있다.
[선점]
- RR(Round-Robin)
라운드 로빈은 프로세스가 도착한 순서대로 처리하지만(FCFS) 각 프로세스에 동일한 시간 할당량(타임 슬라이스)을 부여하고, 시간 할당량이 완료되면 다음 프로세스로 전환한다. 이 알고리즘은 공정성을 제공하며, 실시간 시스템에서 널리 사용된다. 모든 프로세스에 공평한 CPU 시간을 할당하지만 타임 슬라이스 크기에 따라 시스템의 성능과 응답 시간이 달라질 수 있다.
- SRT(Shortest-Remaining-Time)
SJF가 비선점형 스케줄링 알고리즘으로, 한 번 CPU를 할당받으면 작업이 완료될 때까지 CPU를 계속 사용하는 반면, SRT는 준비 상태에 있는 프로세스 중 남은 실행 시간이 가장 짧은 프로세스를 선택하여 실행하는 선점형 스케줄링 알고리즘이다. 새로운 프로세스가 도착하거나 실행 중인 프로세스보다 더 짧은 실행 시간을 가진 프로세스가 준비 상태가 되면, 현재 실행 중인 프로세스를 중단하고 새로운 프로세스로 전환한다.
프로세스 A | 프로세스 B | 프로세스 C |
실행 시간 10초 | 실행 시간 2초 | 실행 시간 4초 |
도착 시간 0초 | 도착 시간 2초 | 도착 시간 4초 |
SJF를 사용할 경우, 프로세스 A가 먼저 도착했지만, B와 C가 더 짧은 실행 시간을 가지고 있다. 그럼에도 불구하고 A가 먼저 실행된다. A가 완료된 후 B, 그 다음 C 순으로 실행된다.
SRT를 사용할 경우, 프로세스 A가 처음에 실행을 시작한다. 2초 후, 프로세스 B가 도착하고, 이 시점에서 A의 남은 시간은 8초, B의 전체 시간은 2초이다. B가 더 짧으므로, 시스템은 A의 실행을 중단하고 B를 실행한다. B가 완료된 후, C가 도착하면 C의 실행 시간과 A의 남은 실행 시간을 비교하여 남은 시간이 더 짧은 작업을 실행한다.
- 우선순위 기반(Priority Scheduling)
우선순위 기반 스케줄링은 각 프로세스에 우선순위를 할당하고, 가장 높은 우선순위를 가진 프로세스부터 스케줄링한다. 우선순위는 프로세스의 중요도나 요구 사항에 따라 결정될 수 있다. 중요한 작업부터 먼저 처리할 수 있지만 낮은 우선순위의 프로세스가 기아 상태에 빠질 수 있다.(우선순위 역전 문제)
- 다단계 큐(Multilevel Queue)
선점 스케줄링 방식으로 프로세스를 여러 큐에 분류하고, 각 큐에 다른 스케줄링 알고리즘을 적용한다. 예를 들어, 시스템 프로세스, 대화형 프로세스, 배치 프로세스 등으로 분류할 수 있다. 다양한 프로세스 유형을 효율적으로 관리할 수 있지만 큐 사이의 프로세스 이동 관리가 복잡할 수 있다.
위 사진처럼, 처음 프로세스가 도착하면 단계 1 큐에 들어간다. 그리고 해당 큐의 시간 할당량만큼 도착한 순서대로 프로세스를 처리한다.(FCFS) 이 때 시간 할당량을 다 썼지만 프로세스가 종료되지 못해싸면 다음 단계의 큐로 이동 배치한다. 2~3의 과정을 반복하며 마지막 n단계 시간 할당량만큼 실행 후 종료되지 못한 경우 RR 스케줄링 방식으로 동작한다.
프로세스 상태
프로세스의 상태는 프로세스가 실행되는 동안 변경되는 고유 상태를 의미한다. 프로세스가 생성되어 실행하기까지 프로세스는 여러가지의 상태를 갖게 되고, 상태의 변화에 따라 프로세스가 동작되는 것이다. 프로세스는 일반적으로 다음과 같은 5가지의 상태를 가진다.
프로세스 상태 | 설명 |
생성 (new) | 프로세스가 생성되고 초기화되는 단계, 프로세스가 메모리에 할당되기 전 상태이다. |
준비 (ready) | 프로세스가 CPU에서 실행될 준비가 완료되고, 실행을 위해 CPU할당을 기다리는 상태이다. |
실행 중 (running) | 프로세스가 CPU를 할당받아 명령어를 실행하는 상태이다. |
대기 (waiting) | 프로세스가 특정 이벤트(예:I/O 작업 완료)의 발생을 기다리는 동안 CPU 실행을 일시적으로 멈춘 상태이다. |
종료 (terminated) 또는 완료 (exit) | 프로세스가 실행을 완료하고 종료된 상태이다. 더 이상 실행될 수 없으며, 메모리에서 제거된다. |
프로세스 상태 전이
프로세스 상태 전이란 프로세스가 실행되는 동안 상태가 OS에 의해 변경되는 것을 말한다. 운영체제는 프로세스의 상태를 감시하고, 프로세스 상태를 기반으로 프로세스 스케줄링을 통해 프로세스를 관리하고 제어한다. 예를 들어, ready 상태인 여러 프로세스 중에서 어떤 프로세스를 running 상태로 바꿀지, terminated 상태에 있는 프로세스를 제거하고 ready 상태에 있는 다른 프로세스를 선택할지 스케줄링 알고리즘에 의해 동작된다.
1. Admitted (new -> ready): 프로세스가 시스템에 의해 초기화되고 실행을 위한 모든 준비가 완료되면 준비 상태로 전이된다.
2. Dispatch (ready -> running): 운영체제가 스케줄러의 준비 상태에 있는 프로세스 중 하나를 선택하여 CPU를 할당하면, 그 프로세스는 실행 중 상태로 전이된다.
3. Interrupt (running -> ready): Timeout, 예기치 않은 이벤트가 발생(타임 슬라이스 만료, 더 높은 우선순위 프로세스 등장 등)하여 현재 실행 중인 프로세스를 준비 상태로 전환하고, 해당 작업을 먼저 처리한다.
4. I/O or event wait (running -> waiting): 프로세스가 I/O 요청과 같은 대기가 필요한 작업을 실행하면, 실행을 일시 중지하고 대기 상태로 전이된다.
5. I/O or event completion (waiting -> ready): 대기 중이던 이벤트나 조건이 충족되면, 프로세스는 다시 준비 상태로 전이되어 CPU할당을 기다린다.
🤯 I/O 요청이 발생했을 때 running->ready가 아니라 running->waiting으로 전이되는 이유는?
결론적으로 프로세스가 입출력 작업을 기다리는 동안 CPU를 효율적으로 활용하기 위해서이다. 프로세스가 I/O 요청을 하고 그 결과를 기다리는 동안에는 CPU에서 할 일이 없게 된다. 이 시간 동안 CPU를 계속 점유하고 있으면, CPU가 다른 작업을 처리하는 데 사용될 수 있는 기회를 낭비하게 된다. 따라서 I/O 작업을 기다리는 동안 프로세스를 대기 상태로 전환시키고, CPU는 다른 준비 상태의 프로세스를 실행할 수 있게 된다.
프로세스 컨텍스트 스위칭
컨텍스트 스위칭(Context Switching)은 CPU가 한 프로세스에서 다른 프로세스로 전환할 때 발생하는 일련의 과정을 말한다. 위의 동시성(Concurrency) 에서 다뤘듯이 CPU는 한 번에 하나의 프로세스만 실행할 수 있으므로, 여러 개의 프로세스를 번갈아가며 실행하여 CPU 활용률을 높이기 위해 컨텍스트 스위칭이 필요한 것이다.
예를 들어, 동작 중인 프로세스 P0이 대기를 하면서 해당 프로세스의 상태(Context)를 보관하고, 대기하고 있던 다음 순서의 프로세스인 P1이 동작하면서 이전에 보관했던 프로세스의 상태를 복구하는 작업을 말한다. 이러한 컨텍스트 스위칭이 일어날 때 다음번 프로세스는 스케줄러가 결정하게 된다. 즉, 컨텍스트 스위칭을 하는 주체는 스케줄러이다.
- PCB(Process Control Block)
PCB(프로세스 제어 블록)는 운영체제에서 프로세스를 관리하기 위해 해당 프로세스의 상태 정보를 담고 있는 자료구조를 말한다.
프로세스를 컨텍스트 스위칭할 때 기존 프로세스의 상태를 어딘가에 저장해 둬야 다음에 똑같은 작업을 이어서 할 수 있을 것이고, 새로 해야 할 작업의 상태 또한 알아야 어디서부터 다시 작업을 시작할지 결정할 수 있을 것이다. 즉, PCB는 프로세스 스케줄링을 위해 프로세스에 관한 모든 정보를 저장하는 임시 저장소이다.
프로세스(Process)가 생성되면 메모리에 해당 프로세스의 PCB가 함께 생성되고, 종료 시 삭제된다.
운영체제는 PCB에 담긴 프로세스 고유 정보를 통해 프로세스를 관리하며, 프로세스의 실행 상태를 파악하고, 우선순위를 조정하며, 스케줄링을 수행하고, 다른 프로세스와의 동기화를 제어한다.
운영체제에 따라 PCB에 포함되는 항목이 다를 수 있지만 일반적으로 PCB 내에는 다음과 같은 정보가 포함되어 있다.
1. 포인터 (Pointer): 프로세스의 현재 위치를 저장하는 포인터 정보
2. 프로세스 상태 (Process State): 프로세스의 각 상태 (new, ready, running, waiting, terminated or exit) 를 저장
3. 프로세스 아이디 (Process Number, PID): 프로세스 식별자를 지정하는 고유한 ID
4. 프로그램 카운터 (Program Counter): 프로세스가 다음에 실행할 명령어의 주소를 가리킨다. CPU가 이 프로세스를 다시 실행할 때, 명령어 실행을 이어갈 위치를 알려준다.
5. 레지스터 (Register): 프로세스가 실행되는 동안 CPU는 다양한 레지스터를 사용하여 명령어 실행에 필요한 데이터를 저장하거나, 명령어 실행 결과를 임시로 저장한다. 컨텍스트 스위칭 과정에서 현재 실행 중인 프로세스의 CPU 레지스터 상태를 PCB에 저장한다. 이는 프로세스가 중단되었다가 재개될 때 원래 상태로 복원되어야 하는 레지스터 값들을 포함한다.
6. 메모리 제한 (Memory Limits): 프로세스가 사용할 수 있는 메모리의 범위. 이는 프로세스가 할당받은 메모리 영역의 시작 주소와 끝 주소, 페이지 테이블에 대한 정보, 메모리 사용량 제한 등을 포함할 수 있다. 메모리 관리 시스템은 이 정보를 사용하여 프로세스가 자신에게 할당된 메모리 영역 내에서만 데이터를 읽고 쓰도록 제한한다.
7. 열린 파일 목록 (Open File Lists): 프로세스를 위해 열린 파일 목록
- Context Switching 과정
1. CPU는 Process P0을 실행한다. (Executing)
2. 일정 시간이 지나 interrupt 또는 system call이 발생한다. (CPU는 idle 상태)
3. 현재 실행 중인 Process P0의 상태를 PCB0에 저장한다. (save state into PCB0)
4. 다음으로 실행할 Process P1을 선택한다. (CPU 스케줄링)
5. Process P1의 상태를 PCB1에서 불러온다. (reload state from PCB1)
6. CPU는 Process P1을 실행한다. (Executing)
7. 일정 시간이 지나 interrupt 또는 system call이 발생한다. (CPU는 idle 상태)
8. 현재 실행 중인 Process P1의 상태를 PCB1에 저장한다. (save state into PCB1)
9. 다시 Process P0을 실행할 차례가 된다. (CPU 스케줄링)
10. Process P0의 상태를 PCB0에서 불러온다. (reload state from PCB0)
11. CPU는 Process P0을 멈췄던 시점부터 실행한다. (Executng)
💡 idle(대기) 와 executing(실행)은 CPU의 동작 상태를 나타낸 것이다.
- Context Switching Overhead
이러한 컨텍스트 스위칭 과정은 사용자로금 빠른 반응성과 동시성을 제공하지만, 실행되는 프로세스의 변경 과정에서 프로세스의 상태, 레지스터 값 등이 저장되고 불러오는 등의 작업이 수행되기 때문에 시스템에 많은 부담을 주게 된다.
위의 컨텍스트 스위칭 과정 그림을 보면 P0이 executing에서 idle이 될 때 P2가 바로 executing이 되지 않고 idle 상태에 있다가 executing 되는 것을 볼 수 있다. 이 간극이 바로 컨텍스트 스위칭 오버헤드(Context Switching Overhead)인 것이다.
컨텍스트 스위칭 오버헤드는 대표적으로 PCB 저장 및 복원 비용, CPU 캐시 메모리 무효화에 따른 비용, 프로세스 스케줄링 비용 때문에 발생한다.
컨텍스트 스위칭 과정에서 PCB를 저장하고 복원하는데 비용이 발생하며, 프로세스 자체가 교체되는 것이니 CPU 캐시 메모리에 저장된 데이터는 무효화가 된다. 이전 프로세스와 관련된 데이터가 CPU 캐시에 남아 있을 수 있기 때문이다. 따라서 새로운 프로세스가 실행되면 캐시에 있는 데이터는 더이상 유효하지 않게 되어, 캐시를 새로운 프로세스의 데이터로 채우는 데 시간이 소요된다. 이는 메모리 접근 시간을 증가시키고 성능을 저하시킬 수 있다. 또한 스케줄링 알고리즘에 따라 프로세스를 선택하는 비용도 만만치 않다.
뒤에서 다루겠지만, 컨텍스트 스위칭은 꼭 프로세스 뿐만 아니라 여러 개의 스레드들 끼리도 스위칭이 일어난다. 보통 멀티 스레드라고 하면 여러 개의 스레드가 동시에 돌아가니 프로그램 성능이 무조건 상승할 거라 예상하지만, 이는 정확하지 않다. 앞서 다뤘던 CPU 코어의 물리적인 제한과 더불어 컨텍스트 스위칭 오버헤드라는 변수 때문에 스레드 교체 과정에서 과하게 오버헤드가 발생하면 오히려 멀티 스레드가 싱글 스레드보다 성능이 떨어지는 현상이 나타날 수 있기 때문이다.
스레드 스케줄링
프로세스 스케줄링과 마찬가지로, 스레드 스케줄링(Thread Scheduling)은 운영체제에서 다중 스레드를 관리하며, CPU를 사용할 수 있는 스레드를 선택하고, CPU를 할당하는 작업을 말한다.
스레드의 우선순위, 실행 시간, 입출력 요청 등의 정보를 고려하여 CPU를 사용할 수 있는 스레드를 선택하는, 스레드 스케줄링 알고리즘은 프로세스 스케줄링 알고리즘과 유사하게 동작한다. 다양한 알고리즘이 있으며, 대표적으로는 RR(Round Robin), 우선순위 기반(Priority-based scheduling), 다단계 큐(Multi-level Queue scheduling) 등이 있다.
다만 스레드 스케줄링은 프로세스 스케줄링과 달리, 하나의 프로세스 내에서 다수의 스레드가 동작하는 형태이기 때문에, 스레드 간의 상호작용과 동기화 문제를 고려해야 한다는 차이점이 존재한다.
스레드 상태
프로세스와 마찬가지로, 스레드에도 상태가 있다. 일반적으로 다음과 같이 5가지 상태를 가진다.
스레드 상태 | 설명 |
New | 스레드가 생성되었지만 아직 실행 준비가 완료되지 않은 상태. 이 상태의 스레드는 실행을 위한 초기 설정이나 자원 할당을 기다리고 있다. (호출되지 않음) |
Ready | 스레드가 실행을 위해 모든 준비를 마치고 CPU 할당을 기다리는 상태. 준비 상태의 스레드는 CPU가 할당되기를 대기하는 준비 큐에 위치한다. |
Running | 스레드가 CPU를 할당받아 실행 중인 상태. 한 번에 하나의 스레드만이 프로세서(CPU)에서 실행될 수 있다.(멀티 코어 프로세서의 경우, 코어 당 하나의 스레드) |
Waiting 또는 Blocked | 스레드가 특정 이벤트(예: I/O 작업 완료, 락(lock)획득, 다른 스레드로부터의 신호 등)의 발생을 기다리는 상태. 대기 상태의 스레드는 CPU를 할당받을 수 없으며, 대기 중인 이벤트가 완료되면 준비 상태로 전환된다. |
Terminated 또는 Exited | 스레드가 작업을 완료하고 종료된 상태. 이 상태의 스레드는 더 이상 실행될 수 없으며, 시스템에서 관련 자원을 회수하고 스레드를 제거한다. |
스레드 상태 전이
스레드 상태 전이 | 설명 |
New | 스레드 객체가 생성되었지만, 아직 start() 메소드가 호출되지 않은 상태 |
Runnable | 스레드가 실행될 준비가 완료되었고, CPU를 할당받기 위해 준비된 상태, start() 메소드 호출 후, 스레드는 실행 가능 상태가 된다. |
Running | 스레드가 CPU를 할당받아 실행 중인 상태, 실행 가능 상태에서 CPU가 할당되면 자동으로 실행 상태가 된다. |
Waiting | 스레드가 특정 조건이 충족될 때까지 기다리고 있는 상태, Object.wait(), Thread.join(), LockSupport.park() 등의 메소드 호출로 이 상태에 들어갈 수 있다. |
Timed Waiting | 스레드가 지정된 시간 동안 기다리는 상태, Thread.sleep(), Object.wait(long timeout), Thread.join(long millis) 등의 메소드 호출로 이 상태에 들어갈 수 있다. |
Blocked | 스레드가 객체의 락을 얻기 위해 대기하는 상태, 다른 스레드가 락을 보유하고 있을 때 이 상태가 된다. |
Terminated | 스레드의 작업이 완료되어 더 이상 실행할 코드가 없는 상태 |
🤯 락(Lock) 의 개념
"락(Lock)" 을 얻는다는 것은 멀티스레딩 환경에서 여러 스레드가 동시에 같은 자원(예: 객체, 파일, 데이터) 에 접근하는 제어하기 위해 사용하는 동기화 메커니즘의 일부이다. 락을 사용하면 한 시점에 하나의 스레드만 특정 자원에 접근할 수 있도록 제한하여 데이터의 일관성과 동시성을 유지할 수 있다.
즉, 락은 프로그래밍에서 공유 자원에 대한 접근을 제어하기 위한 메커니즘으로, 특정 코드 영역(임계 영역, Critical Section)을 실행하는 동안 해당 자원을 사용하는 스레드가 다른 스레드로부터 간섭받지 않도록 보호한다.
여기서 임계 영역(Critical Section) 이란 여러 스레드에 의해 동시에 실행되면 안되는 코드 영역을 말한다. 이 영역에는 공유 데이터를 수정하는 코드가 포함될 수 있다.
락을 얻는 과정은 다음과 같다.
1. 락 요청: 스레드가 임계 영역에 진입하려고 할 때, 먼저 해당 자원에 대한 락을 요청한다.
2. 락 할당: 해당 자원에 대한 락이 현재 사용 가능한 상태(즉, 다른 스레드가 보유하고 있지 않은 상태) 이면, 요청한 스레드에게 락이 할당된다. 이 스레드는 임계 영역의 코드를 안전하게 실행할 수 있다.
3. 락 보유 중인 상태: 락을 할당받은 스레드는 해당 자원을 독점적으로 사용할 수 있으며, 다른 스레드는 그 자원에 접근할 수 없다.
4. 락 해제: 스레드가 임계 영역의 작업을 완료하면 락을 해제한다. 이는 다른 스레드가 해당 자원에 접근할 수 있도록 만든다.
위에서 언급한 블록(Blocked) 상태란 스레드가 락을 요청했지만, 이미 다른 스레드가 해당 락을 보유하고 있어서 즉시 임계 영역에 진입할 수 없는 경우, 스레드는 블록 상태가 된다. 이 상태의 스레드는 대기 큐에 들어가 락이 해제될 때까지 기다린다. 락을 보유하고 있는 스레드가 작업을 완료하고 락을 해제하면, 대기 큐에 있는 스레드 중 하나가 락을 할당받고 임계 영역에 진입할 수 있게 된다.
//자바 스레드 프로그래밍 예시
//스레드의 생성, 실행, 대기, 시간 대기, 종료 상태를 보여준다.
public class ThreadExample {
public static void main(String[] args) throws InterruptedException {
// 스레드 생성
Thread thread = new Thread(() -> {
try {
// 시간 대기 상태로 전환
System.out.println("Thread is going to sleep.");
Thread.sleep(2000); // 2초 동안 대기
System.out.println("Thread is woken up.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 스레드 실행
System.out.println("Starting thread.");
thread.start();
// 메인 스레드를 잠시 대기 상태로 전환하여 자식 스레드가 끝날 때까지 기다림
System.out.println("Waiting for the thread to finish.");
thread.join();
System.out.println("Thread has finished execution.");
}
}
스레드 컨텍스트 스위칭
스레드 컨텍스트 스위칭(thread context switching)은 멀티 스레딩 환경에서 스레드 간의 실행을 전환하는 기술이다. 프로세스 컨텍스트 스위칭과 다른 점은 스레드 컨텍스트 스위칭은 하나의 프로세스 내의 스레드들을 교환한다는 점이다.
- TCB(Thread Control Block)
PCB처럼, TCB(스레드 제어 블록)는 각 스레드마다 운영체제에서 유지하는 스레드에 대한 정보를 담고 있는 자료구조이다.TCB는 PCB 안에 들어있다. 스레드가 프로세스 내에 위치한 것처럼 말이다.
PCB와 TCB는 LinkedList로 구현되어 있으며, TCB 역시 스레드의 상태 정보, 스레드 ID, 스레드 우선순위, 스케줄링 정보 등 다양한 정보를 저장한다. TCB도 스레드가 생성될 때 운영체제에 의해 생성되며, 스레드가 실행을 마치고 소멸될 때 함께 소멸된다.
또한 스레드 간의 자원 공유와 동기화도 TCB를 사용하여 관리된다. 예를 들어, 뮤텍스(mutual exclusion)나 세마포어(semaphore)와 같은 동기화 기법을 사용할 때, TCB에서 해당 스레드의 뮤텍스나 세마포어 정보를 관리하고, 스레드가 해당 자원에 대한 접근 권한을 획득하거나 반납할 때 TCB의 정보를 업데이트하게 된다.
💡
뮤텍스(mutual exclusion, mutext): 임계 영역에 1개의 스레드만 들어갈 수 있는 동기화 기법
세마포어(semaphore): 임계 구역에 여러 스레드가 들어갈 수 있고, counter을 두어서 허용 가능한 스레드를 제한하는 기법
- 프로세스 컨텍스트 스위칭 vs 스레드 컨텍스트 스위칭
프로세스 컨텍스트 스위칭과 스레드 컨텍스트 스위칭은 모두 멀티태스킹 환경에서 여러 프로세스 또는 스레드를 동시에 실행하기 위한 기술이다. 그러나 두 기술은 몇가지 차이점이 있다.
1. 컨텍스트 스위칭 속도
TCB는 스레드의 상태 정보를 저장하는 구조체이다. 스레드는 프로세스 내에서 실행되는 경량 실행 단위로, 프로세스 코드, 데이터, 힙 영역 등을 공유한다. TCB에는 스레드 고유의 스택, 프로그램 카운터, 레지스터 세트 등의 정보가 포함된다. PCB는 프로세스의 상태 정보를 저장하는 구조체이다. 프로세스는 독립된 메모리 공간을 가지며, 실행에 필요한 코드, 데이터, 힙, 스택 등의 정보와 함께 실행 상태, 프로세스 ID 등의 메타데이터를 포함한다.
스레드 컨텍스트 스위칭은 프로세스 컨텍스트 스위칭보다 더 가볍고 빠르다. 이는 스레드 간 스위칭 시 공유되는 메모리(코드, 데이터, 힙)가 변경되지 않고, 주로 스택과 CPU 레지스터 값만 갱신되기 때문이다. 반면 프로세스 간 스위칭은 메모리 공간 전체를 교체해야 하므로 더 많은 시간과 자원이 소모된다.
2. 캐시 메모리 초기화 여부
CPU 캐시는 CPU와 메인 메모리 사이에 위치하며 CPU에서 한 번 이상 읽어들인 메모리의 데이터를 저장하고 있다가, CPU가 다시 그 메모리에 저장된 데이터를 요구할 때, 메인 메모리를 통하지 않고 곧바로 데이터를 전달해 주는 용도이다. 즉 자주 접근하는 데이터와 명령어를 빠르게 로드하기 위해 사용되는 소량의 고속 메모리이다.
그런데 프로세스 컨텍스트 스위칭이 일어날 경우, 다른 프로세스의 실행으로 인해 CPU가 새로운 명령어와 데이터를 로드해야 하기 때문에 CPU 캐시 메모리가 초기화되거나 재로드된다. 이 과정에서 성능 저하가 발생할 수 있다.
스레드 컨텍스트 스위칭일 경우, 같은 프로세스 내에서 발생하므로, 프로세스 내 스레드 간 스택과 레지스터 값 등 일부 컨텍스트 정보만 변경되므로 대부분의 캐시된 데이터가 유효하게 유지되어 캐시 무효화가 덜 발생한다. 다만 스레드가 다른 CPU 코어에서 실행될 때는 해당 코어의 캐시 메모리에 스레드 컨텍스트 정보가 로드되어야하므로 초기화될 수 있다.
3. 자원 동기화 문제
멀티스레딩 환경에서 여러 스레드가 공유 자원(예: 힙 영역의 데이터)에 접근할 때, 동시에 같은 자원을 수정하려고 하면 데이터 일관성 문제가 발생할 수 있다. 이러한 문제를 경쟁 상태(Race Condition)라고 하며, 이를 방지하기 위해 뮤텍스, 세마포어 등의 동기화 기법을 사용한다.
프로세스 간에도 공유 자원(예: 파일 시스템, 데이터베이스)에 접근할 경우 동기화 문제가 발생할 수 있다. 프로세스 간 통신(IPC) 메커니즘을 통해 자원 접근을 조율한다.
동기화를 잘못 관리하면 성능 저하나 데드락과 같은 문제가 발생할 수 있다. 따라서 동기화와 자원 관리에 주의를 기울여야 한다.
💡 데드락(Deadlock)
데드락은 두 개 이상의 프로세스나 스레드가 서로 다른 자원을 요청하면서 무한히 대기하는 상태를 말한다. 각 프로세스나 스레드가 이미 점유하고 있는 자원을 보유한 채로 다른 프로세스나 스레드가 점유하고 있는 자원을 추가로 요청하고, 그 요청이 서로에 의해 영원히 충족되지 않을 때 발생한다. 이 상태가 되면 해당 프로세스나 스레드들은 더이상 진행할 수 없게 되며, 시스템의 일부가 멈추게 된다.
데드락이 발생하기 위한 네 가지 필수 조건은 다음과 같다.
1. 상호 배제(Mutual Exclusion): 한 번에 한 프로세스(또는 스레드)만 자원을 사용할 수 있다.
2. 점유 대기(Hold and Wait): 최소한 하나의 자원을 점유한 상태에서 다른 프로세스가 점유하고 있는 추가 자원을 대기하는 프로세스가 존재한다.
3. 비선점(No preemption): 자원이 강제로 회수될 수 없고, 점유하고 있는 프로세스가 자발적으로만 자원을 놓아줄 수 있다.
4. 순환 대기(Circular Wait): 대기하는 프로세스들 사이에 순환 형태의 체인이 존재하며, 각 프로세스는 다음 프로세스가 요구하는 자원을 점유하고 있다.
동기화를 잘못 관리할 때 데드락이 발생하는 이유는 여러 프로세스나 스레드가 자원에 안전하게 접근하기 위해 사용하는 락(lock)이나 세마포어(semaphore)와 같은 동기화 메커니즘이 서로를 기다리는 상황이 발생하기 때문이다. 이러한 상황은 프로세스나 스레드가 필요한 자원을 모두 확보하지 못하고, 서로가 점유하고 있는 자원을 요청하는 순환 대기 상태에 빠지게 만든다.
Multi Process와 Multi Thread
멀티 프로세스와 멀티 스레드는 한 어플리케이션에 대한 처리방식이라고 보면 된다. 이름처럼 멀티 프로세스는 여러 개의 프로세스, 멀티 스레드는 여러 개의 스레드가 동작하는 것을 말한다.
멀티프로세스는 여러 개의 독립된 프로세스가 동시에 실행되는 방식이다. 각 프로세스는 별도의 메모리 공간(코드, 데이터, 힙, 스택 등)을 가지며, 다른 프로세스와 자원을 공유하지 않는다.
프로세스 간 메모리가 분리되어 있어, 한 프로세스의 오류가 다른 프로세스에 영향을 미치지 않는다. 또한 하나의 프로세스가 실패해도 시스템 전체에 치명적인 영향을 주지 않고, 오류를 격리하여 처리할 수 있다.
그러나 각 프로세스마다 별도의 메모리 공간과 시스템 자원을 할당받아 사용하기 때문에, 자원 소모가 크고 프로세스 간 통신(IPC)을 위한 추가적인 설정과 자원이 필요하며, 통신 비용이 상대적으로 높다. 따라서 웹 서버와 같이 각 요청을 독립적으로 처리해야 하며, 하나의 요청 처리가 다른 요청 처리에 영향을 미치지 않아야 하는 경우 멀티프로세스 방식이 적합하다.
멀티스레드는 하나의 프로세스 내에서 여러 개의 스레드가 동시에 실행되는 방식이다. 모든 스레드는 부모 프로세스의 메모리 공간을 공유한다.
스레드 간 메모리를 공유하기 때문에, 프로세스를 생성하는 것보다 자원 소모가 적고 효율적이다. 하나의 스레드가 입출력 작업을 기다리는 동안, 다른 스레드가 계산 작업을 계속 수행할 수 있어 프로그램의 응답성이 향상된다.
그러나 스레드 간 메모리를 공유하기 때문에, 공유 데이터에 대한 접근을 제어하기 위한 동기화 처리가 필요하다. 이는 복잡도를 증가시키고 성능 저하를 일으킬 수 있다. 그리고 하나의 스레드에서 발생한 문제가 전체 프로세스에 영향을 줄 수 있어 프로그램 안정성이 떨어질 수 있다. 따라서 GUI 애플리케이션, 웹 브라우저, 서버 애플리케이션과 같이 사용자의 입력 처리와 데이터 처리를 동시에 해야 하며, 자원을 효율적으로 사용해야 하는 경우 멀티스레드 방식이 적합하다.