물리 스레드와 가상 스레드

#cs
// INDEX

CPU, 코어, 프로세스, 스레드의 개념

CPU

CPU 는 중앙 처리 장치라는 이름에서 알 수 있듯이, 프로그램의 명령을 처리하는 핵심적인 장치다. 컴퓨터의 중심에서 데이터 처리를 담당하며, 주어진 명령을 하나씩 차례대로 수행한다. 이러한 명령들은 보통 프로그램에 작성된 코드이며, 실행 중인 프로그램을 CPU 가 처리한다. CPU 의 성능은 코어 수, 클럭 속도, 아키텍처, 하이퍼 스레딩 등의 요소에 의해 결정된다.

코어

CPU 의 코어는 실제로 명령을 처리하는 독립적인 연산 장치다. 각 코어는 물리적으로 독립된 처리 유닛으로, 명령을 순차적으로 처리하는 역할을 한다.
멀티코어 CPU 는 여러 개의 코어를 갖고 있어, 각 코어가 병렬적으로 스레드를 처리할 수 있다. 코어가 많을수록 동시 처리 능력이 향상되어 멀티태스킹이나 다중 스레드 작업에서 성능이 증가한다. 다만 발열등의 물리적인 한계 때문에 CPU 코어의 수는 한계가 존재한다.

프로세스

프로세스는 운영체제로부터 메모리 공간을 할당받아 실행중인 프로그램을 의미한다. 파일로 저장 보조 저장장치에 저장되어 있는 아직 실행되지 않은 상태의 코드를 프로그램이라고 한다. 운영체제는 디스크에 저장된 프로그램을 메모리 영역에 로드하고, 레지스터, 프로그램 카운터, 메모리 등을 할당한다. 운영체제가 프로세스에 할당하는 메모리는 크게 4가지 영역으로 나뉠 수 있다.

  • 코드 영역: 프로그래머가 작성한 코드 영역
  • 데이터 영역: 전역적으로 사용하는 전역 변수, 정적 변수, 상수 등에 대한 영역
  • 힙 영역: 프로그래머가 관리하는 메모리 영역으로, 생성자, 인스턴스 등 동적으로 할당되는 데이터 영역
  • 스택 영역: 함수 호출과 관련된 메모리 영역

각 프로세스는 고유의 메모리 공간을 운영체제에 의해 할당받아 실행되며, 각각의 프로세스는 독립적으로 실행된다. 이는 서로 다른 프로세스의 자원에 접근할 수 없으며, 공유가 필요한 경우 IPC 와 같은 통신방식을 통해 가능하나, 프로그래밍의 난이도가 높아진다. 프로세스간 컨텍스트 스위칭은 상당히 높은 비용이 들기 때문에, 동시적인 작업이 필요한 경우 멀티 프로세스 보다는 멀티 스레드 방식을 이용하는 것이 효율적일 수 있다.

스레드

스레드는 프로세스 내에서 실행 가능한 가장 작은 단위다. 하나의 프로세스는 여러 스레드를 포함하며, 각 스레드는 프로세스상의 코드, 데이터, 힙 영역의 메모리 공간을 공유한다. 이는 자원의 중복성을 스레드간 최소화할 수 있기 때문에 성능적인 부분에 대한 이점을 가질 수 있다. 다만, 프로세스에 할당된 메모리 공간을 여러 스레드가 함께 사용하기 때문에 공통 영역의 데이터 접근과 처리를 올바르게 해야한다. 그렇지 않는 경우 동시성 문제, 동기화 문제가 발생할 수 있다.

스레드는 크게 사용자 스레드와 커널 스레드로 구분된다. 사용자가 작성한 프로그램 내에서 실행되는 스레드가 사용자 스레드이고, 운영체제에서 직접 관리하는 스레드는 커널 스레드다.


물리적 스레드와 가상 스레드

물리적 스레드(커널 스레드)

커널 스레드는 운영체제가 직접 관리하는 스레드로, CPU 코어와 1:1로 매핑되어 스케줄링된다. 멀티코어 환경에서는 여러 프로세스가 병렬로 실행될 수 있으며, 운영체제가 이 작업을 관리한다. 하나의 CPU 코어에서 여러 스레드가 실행되어야 하는 경우, CPU 는 컨텍스트 스위칭을 통해 스레드를 전환한다.

컨텍스트 스위칭은 작업의 진행 상황을 저장하고 다른 작업으로 전환하는 과정으로, 이 과정은 cpu 의 처리속도에 상당한 영향을 끼친다.

가상 스레드(사용자 스레드)

가상 스레드는 운영체제의 커널 스레드와 독립적으로 관리되는 경량 스레드다. 가상 스레드는 사용자 스레드라고도 불리며, 여러 가상 스레드가 하나의 커널 스레드 위에서 동작할 수 있다. 이런 구조 덕분에 수많은 가상 스레드를 동시에 생성하고 실행할 수 있어, 동시 작업을 매우 효율적으로 처리할 수 있다.

가상 스레드는 보통 런타임 환경이나 언어 프레임워크에서 관리되며, 운영체제의 커널 스레드와 1:1로 매핑되지 않는다. 예를 들어, 자바의 가상 스레드(프로젝트 Loom)나 Go의 고루틴이 가상 스레드의 대표적인 사례이다.

특징

  • 경량성: 가상 스레드는 커널 스레드보다 훨씬 적은 메모리와 자원을 사용한다. 예를 들어, 커널 스레드는 보통 스택 메모리와 관련된 고정된 자원을 많이 소모하지만, 가상 스레드는 매우 적은 메모리로 수천, 수만 개의 스레드를 생성할 수 있다.
  • 비선점형 스케줄링: 가상 스레드는 보통 비선점형 스케줄링 방식을 사용한다. 이는 가상 스레드가 자발적으로 CPU 사용을 중단할 때까지 실행된다는 의미다. 만약 스레드가 I/O 작업으로 블로킹되면 해당 스레드는 CPU 사용을 중단하고, 런타임 환경이 다른 가상 스레드에 CPU를 할당해 동시성 처리가 가능하다.
  • 멀티플렉싱: 여러 가상 스레드가 하나의 커널 스레드 위에서 번갈아 가며 실행된다. 가상 스레드는 I/O 같은 작업 중에 비활성화되고, 그 사이에 다른 가상 스레드가 CPU를 사용할 수 있게 되는 방식이다. 이로 인해 물리적인 CPU 자원을 더욱 효율적으로 사용할 수 있다.
  • 높은 동시성 처리: 위와 같은 특징들로 인해 가상 스레드는 많은 수의 동시 작업을 커널 스레드보다 효율적으로 처리할 수 있다. 네트워크 I/O, 파일 I/O 같은 블로킹 작업을 비선점형 방식으로 스케줄링하고 컨텍스트 스위칭 비용이 가볍기 때문에 커널 스레드에 비해 많은 수의 스레드에서도 성능 저하가 적다.

예를 들어, Go 언어의 고루틴은 가상 스레드의 대표적인 사례다. 고루틴은 매우 적은 메모리만을 사용하여 생성되며, 수십만 개의 고루틴을 동시에 실행할 수 있다. Go의 런타임 스케줄러는 고루틴을 효율적으로 관리하고, 여러 고루틴이 하나의 커널 스레드 위에서 번갈아가며 동작한다. 이는 고성능 네트워크 서버나 대규모 동시 작업을 요구하는 애플리케이션에서 큰 성능 향상을 가져올 수 있다.


가상 스레드 스케줄링과 비동기 I/O

가상 스레드의 스케줄링 방식

가상 스레드는 전통적인 운영체제의 스케줄링이 아니라, 각 프로그래밍 언어나 런타임 환경이 자체적으로 관리하는 방식으로 동작한다. 예를 들어 Go 언어는 자체 런타임 스케줄러를 사용하여 고루틴(가상 스레드)의 스케줄링을 관리하며, 비선점형 스케줄링 방식을 채택한다. 이 방식에서는 가상 스레드가 자발적으로 CPU 사용을 중단하기 전까지 작업이 실행된다.

비선점형 스케줄링에서는 각 가상 스레드가 CPU 를 점유한 상태에서 자신이 완료되거나, 블로킹 작업(예: I/O 작업)이 발생할 때까지 다른 스레드로 전환되지 않는다. 특히 블로킹 I/O가 발생하면 가상 스레드는 대기 상태로 전환되고, 그동안 다른 작업이 진행된다. 이를 통해 CPU 자원을 효율적으로 사용하면서 높은 동시성을 유지할 수 있다.

I/O와 가상 스레드의 시너지

가상 스레드는 I/O 작업에서 특히 큰 장점을 발휘한다. I/O 작업에는 두 가지 방식이 있다: 블로킹 I/O와 논블로킹 I/O다.

블로킹 I/O는 요청이 완료될 때까지 스레드가 해당 작업에 묶여 있는 방식이다. 예를 들어, 파일을 읽거나 네트워크 요청을 보내면 응답이 도착할 때까지 다른 작업을 할 수 없고 대기해야 한다. 블로킹 I/O의 경우 가상 스레드는 대기 상태로 전환되며, 이때 CPU는 다른 가상 스레드로 작업이 전환된다. 따라서 I/O 요청이 많을 때에도 자원을 효율적으로 사용할 수 있다. 즉, 블로킹 I/O 작업이 발생하더라도 다른 가상 스레드가 CPU 를 활용할 수 있기 때문에, 전체적으로 자원의 사용 효율성이 높아진다.

논블로킹 I/O는 요청을 보낸 후 작업이 완료되지 않아도 계속해서 작업을 수행할 수 있다. 논블로킹 I/O는 요청된 작업이 완료되는 즉시 알림을 받거나, 결과가 준비되었을 때만 다시 가상 스레드를 활성화시킨다. 이를 통해 가상 스레드는 I/O 작업에서 CPU 자원을 더 효율적으로 사용할 수 있고, 동시에 더 많은 작업을 병렬로 처리할 수 있다.

이러한 특성 덕분에 가상 스레드는 블로킹 I/O 작업에서도 일정한 성능을 유지할 수 있지만, 논블로킹 I/O와 결합되면 그 효율성이 더욱 극대화된다. 특히 고성능 서버나 대규모 네트워크 요청이 발생하는 환경에서 매우 유리하다.

가상 스레드의 문제점과 한계

가상 스레드는 메모리와 리소스를 적게 사용하는 장점이 있지만, 이론적으로 무한대로 생성할 수는 없다. 네트워크 I/O 작업이 대규모로 발생할 경우 다음과 같은 문제들이 발생할 수 있다.

  • 메모리 고갈: 가상 스레드는 상대적으로 경량화되어 있지만, 생성 시마다 최소한의 스택 메모리와 메타데이터를 차지한다. 대규모로 스레드를 생성할 경우 시스템의 물리적 메모리가 부족해질 수 있다. 이를 방지하려면 애플리케이션에서 가상 스레드의 수를 적절히 관리하거나 스레드 풀을 활용하여 스레드를 재사용하는 방식 등을 사용할 수 있다.
  • 스케줄링 오버헤드: 가상 스레드의 수가 많아지면 런타임 스케줄러가 더 많은 작업을 처리해야 하기 때문에 스케줄링과 컨텍스트 스위칭의 오버헤드가 증가한다. 이는 시스템 성능 저하로 이어질 수 있으므로, 스레드 수를 운영하는 환경에 맞춰 최적화하는 것이 중요하다.
  • 소켓 자원 고갈: 많은 가상 스레드가 네트워크 작업을 처리할 경우, 프로세스에 할당된 사용 가능한 소켓 핸들이 부족해질 수 있다. 운영체제의 소켓 자원은 한정적이기 때문에 소켓 자체가 고갈될 수 있어, 대규모 네트워크 요청 시 소켓 자원 관리도 필수적이다.
  • 디스크 I/O 자원 고갈: 대규모 디스크 I/O 작업이 발생할 경우, 파일 핸들이나 디스크 큐에 대한 요청이 폭주하여 시스템 자원이 부족해질 가능성이 있다. 예를 들어, 파일을 동시에 열거나 쓸 수 있는 최대 한계를 초과하면 더 이상의 I/O 요청이 실패할 수 있어, 대규모 디스크 I/O 시 자원에 대한 고려가 필요하다.
  • 네트워크 및 디스크 대역폭 한계: 가상 스레드가 대규모로 I/O 요청을 발생시키면, 네트워크 또는 디스크 대역폭이 포화 상태가 될 수 있다. 이는 응답 시간이 길어지거나 데이터 전송 속도가 감소하는 문제를 일으킬 수 있다. 따라서 I/O 작업이 많은 경우에는 네트워크와 디스크 대역폭을 고려하여 설계가 필요하다.