스레드

상훈·2025년 5월 7일

OS study

목록 보기
6/6

스레드

  1. 스레드란?

    • 프로세스와 스레드
  2. 사용자 수준 스레드 vs 커널 수준 스레드

  3. 자바의 스레드와 가상 스레드

1. 스레드란

프로세스의 두 가지 특성인 자원과 제어 중 제어만 분리한 실행 단위

프로세스

  • 메모리에 올라가 실행 중인 프로그램
  • 메모리 구조 + 레지스터 Set

스레드는 이중에서 레지스터 Set과 스택을 가진다.

위 이미지와 같이 스레드는 각각의 독립적인 스레드 실행 환경 정보(레지스터 Set)와 지역 데이터 그리고 스택을 가지며 프로세스의 힙, 데이터, 코드 영역을 공유하여 사용한다.

이때, 프로세스를 중량 프로세스(HWP), 스레드를 경량 프로세스(LWP)라고도 한다.

다중 프로세스 vs 다중 스레드

다중 프로세스와 다중 스레드는 동시성 또는 병렬성을 달성하기 위한 두 가지 주요한 방법으로 어떤 상황에 어떤 것을 사용하는 것이 좋을지는 해결하려는 문제의 특성, 자원 공유의 필요성, 안정성 요구사항 등에 따라 달라진다.

1. 다중 프로세스 (Multi-processing)를 사용하는 것이 좋은 상황

각 프로세스가 독립적인 메모리 공간을 가지므로, 안정성이 중요하거나, 각 작업이 다른 작업에 영향을 주지 않아야 하거나, 여러 CPU 코어를 최대한 활용해야 하는 CPU 집약적인 작업에 유리

  • 웹 브라우저
    • 여러 탭, 확장 프로그램을 동시에 실행
      • 하나의 탭(프로세스)이나 확장 프로그램에서 오류가 발생하거나 충돌해도 다른 탭이나 브라우저 전체가 종료되는 것을 방지
  • 대규모 데이터 처리 및 분석
    • 대용량 데이터를 여러 CPU 코어를 사용하여 병렬로 분석하거나 변환할 때
      • 각 데이터를 별도의 프로세스에 할당하여 여러 CPU 코어에서 동시에 처리 가능. Python의 경우는 GIL(Global Interpreter Lock)을 우회하여 병렬 처리 가능
  • 독립적인 여러 서비스 모듈 실행
    • 하나의 큰 시스템이 서로 다른 기능을 수행하는 여러 독릭접인 서비스 모듈로 구성되어 있을 때

2. 다중 스레드 (Multi-threading)를 사용하는 것이 좋은 상황

다중 스레드는 같은 프로세스 내에서 메모리를 공유하므로, 데이터 공유가 쉽고 빠르며, 생성 및 문맥 교환 비용이 적음. 주로 I/O 대기가 잦은 작업이나, 빠른 응답성이 중요한 애플리케이션, 자원을 효율적으로 공유해야 하는 상황에 유리

  • 웹 서버
    • 수많은 클라이언트로부터 동시에 요청을 받아 처리
      • 한 스레드가 클라이언트 요청을 처리하다가 데이터베이스 응답을 기다리는 동안(I/O 대기), 다른 스레드는 다른 클라이언트의 요청을 처리 가능
  • GUI 애플리케이션(워드 편집기)
    • 사용자 입력에 즉각적으로 반응하면서, 백그라운드에서는 파일 저장, 맞춤법 검사 작업을 수행
      • 메인 스레드는 사용자 인터랙셔 처리에 집중하고, 오래 걸리거나 블로킹될 수 있는 작업은 별도의 작업 스레드에 위임

2. 스레드의 구현

스레드는 운영체제에 따라 다양하게 구현할 수 있는데, 대부분 다음 세 가지 형태로 구현한다.

  • 사용자 수준 스레드 : 다대일 (n : 1) 매핑
    • 스레드 라이브러리를 이용하여 작동하는 형태
  • 커널 수준 스레드 : 일대일 (1 : 1) 매핑
    • 커널(운영체제)에서 지원하는 형태
  • 혼합형 스레드 : 다대다 (n : m) 매핑

사용자 수준 스레드

  • 사용자 영역의 스레드 라이브러리로 구현
  • 스레드와 관련된 모든 행위를 사용자 영역에서 하므로 커널이 스레드의 존재를 모름
  • 스레드 라이브러리를 통해 스레드의 생성과 종료, 스레드 간의 메시지 전달, 스레드의 스케줄링과 문맥 등 정보를 보관

장점

  • 매우 빠른 생성 및 문맥 교환
    • 스레드 생성, 동기화, 스케줄링 등이 모두 사용자 공간에서 라이브러리 함수 호출로 이루어지므로 커널 모드로의 전환(시스템 콜)이 필요 없어 오버헤드가 매우 적음
  • 이식성
    • 운영체제의 스레드 지원 여부와 상관없이 스레드 라이브러리만 있으면 구현 가능

단점

  • 블로킹 시스템 콜 문제(동시성 지원 X)
    • 하나의 사용자 수준 스레드가 입출력 작업 등으로 인해 블로킹 시스템 콜을 호출하면, 커널은 해당 프로세스 전체가 블록된 것으로 간주하여 프로세스 내의 모든 사용자 수준 스레드가 함께 블록됨
  • 멀티 코어 활용 불가
    • 모든 사용자 수준 스레드가 결국 하나의 커널 스레드 위에서 동작하므로, 여러 CPU 코어가 있어도 동시에 여러 사용자 수준 스레드를 병렬로 실행할 수 없음

라이브러리를 통해 구현되어 가볍고 빠르지만, 커널의 지원 한계로 인해 동시성과 병렬성에 제약이 따름

커널 수준 스레드

  • 커널이 스레드와 관련된 모든 작업을 관리(PCB와 TCB)

장점

  • 블로킹 문제 해결
    • 하나의 스레드가 블로킹 시스템 콜을 호출해도, 다른 스레드들은 커널에 의해 계속 스케줄링되어 실행 가능(프로세스 전체가 멈추지 않음)
  • 멀티코어 활용 가능
    • 커널이 각 스레드를 개별적으로 인식하고 스케줄링하므로, 여러 CPU 코어에 스레드들을 분산시켜 병렬 실행 가능

단점

  • 오버헤드
    • 스레드 생성, 관리, 문맥 교환 시 커널 모드로 전환(시스템 콜)이 필요하므로 사용자 수준 스레드보다 오버헤드가 더 큼

운영체제의 완전한 지원을 받아 강력한 동시성 및 병렬성을 제공하며, 대부분의 현대 운영체제(Windows, Linux, macOS)에서 표준 스레드 모델로 사용

혼합형 스레드

사용자 수준 스레드와 커널 수준 스레드를 혼합한 구조

  • 시스템 호출을 할 때는 다른 스레드를 중단하는 다대일 (n : 1) 매핑의 사용자 수준 스레드 문제 극복
  • 스레드 수를 제한하는 일대일 (1: 1) 매핑의 커널 수준 스레드 문제 극복

사용자 수준 스레드는 라이브러리가, 커널 수준 스레드는 운영체제 커널이 관리. 주로 라이브러리가 사용자 스레드를 생성된 커널 스레드 풀에 적절히 할당하고 관리

장점

  • 사용자 수준 스레드의 빠른 생성/관리 이점과 커널 수준 스레드의 병렬성 및 블로킹 문제 해결 능력을 결합

  • 하나의 사용자 스레드가 블로킹되어도, 같은 프로세스 내 다른 사용자 스레드가 다른 커널 스레드에서 계속 실행될 수 있음

단점

  • 구현 복잡도가 높음
    • 사용자 수준 라이브러리와 커널 간의 조율, 스레드 매핑 및 스케줄링 등이 까다로움

3. 자바 스레드

자바의 스레드는 Java에서 전통적으로 사용해온 스레드 모델인 플랫폼 스레드(Platform Thread)와 Java 19부터 도입된 가상 스레드(Virtual Thread)로 나눌 수 있음

플랫폼 스레드(java.lang.Thread)

운영체제(OS)가 직접 관리하는 네이티브 스레드(Native Thread)와 일대일로 매핑되는 자바 스레드

우리가 일반적으로 new Thread().start() 구문으로 생성하는 스레드로 현대 대부분의 OS에서 커널 스레드와 1:1로 매핑됨

  1. new Thread() - 사용자 공간 객체 생성

    • Thread t = new Thread(runnable);
    • 이 코드가 실행되면, 자바 힙 메모리에 java.lang.Thread 클래스의 인스턴스가 생성됨
    • 이 Thread 객체는 자바 수준의 스레드 표현체(사용자 수준 스레드). 커널 수준 스레드와의 인터페이스로 사용자 핸들이라고 표현하기도 함
      • 스레드 이름, 우선순위(자바 수준), 상태, 실행한 Runnable 객체 참조, 그룹 정보 등이 저장됨
    • 이 시점에서는 아직 OS 수준의 커널 스레드가 생성되지는 않음. 단순히 자바 객체만 만들어진 상태
  2. t.start() - 커널 스레드 생성 및 실행 요청

    • 네이티브 메소드 호출 : start() 메서드는 내부적으로 start0()라는 private native 메서드를 호출

  • 네이티브 스레드 구조체 초기화
    • 커널 스레드 정보와 자바 스레드 관련 정보를 갖고 있는 JVM의 자료 구조
  • 이 시점에 nativate method(JNI를 통해)를 통해 OS에 커널 스레드 생성 요청 (System Call)
    • 이제 사용자 수준 스레드와 커널 스레드 모두 존재
  • 커널 스레드와 사용자 수준 스레드 연결 (네이티브 스레드 구조체에 정보 할당, run() 메서드를 실행하기 위한 정보)

가상 스레드

우아한 기술 블로그 : https://techblog.woowahan.com/15398/ 이미지 참고

  • 기존 사용자 수준 스레드(플랫폼 스레드)위에 가상 스레드가 존재하는 형태

  • CarrierThread (플랫폼 스레드)
    • 전통적인 자바 스레드로, 운영쳉제의 커널 스레드와 1:1로 매핑되는 상대적으로 무거운 스레드
    • 커널 스레드는 가상 스레드를 직접 알지 못하기에, 플랫폼 스레드를 통해 가상 스레드가 실행된다. 따라서 플랫폼 스레드가 가상 스레드를 업고있다 해서 캐리어 스레드라고도 불림
  • ForkJoinPool
    • java.util.concurrent 패키지에 포함된 스레드 풀
    • 캐리어 스레드를 관리하고, 가상 스레드들을 캐리어 스레드에 할당하는 스케줄러 역할 수행
  • Continuation
    • 프로그램의 특정 지점에서 실행 상태를 캡처했다가, 나중에 그 상태에서 실행을 재개할 수 있도록 하는 문맥 정보
    • 가상 스레드가 블로킹될 때 현재 실행 상태와 스택이 Continuation에 저장됨
    • 가상 스레드가 실행될 때 캐리어 스레드에 마운트(mount) 되어 실행
  • Work Stealing 방식
    • 풀 내의 각 워커 스레드는 자신 만의 작업 큐를 가짐
    • 만약 어떤 워커 스레드가 자신의 큐에 있는 모든 작업을 다 처리해서 일이 없으면, 다른 워커 스레드의 작업 큐의 꼬리(tail)에서 작업을 훔쳐와서 처리
흐름

Virtual Thread2가 unpark()되면 Virtual Thread2의 Continuation은 ForkJoinPool에 제출된다. 이후 ForkJoinPool은 전달 받은 continuation을 캐리어 스레드의 work queue에 할당하여 실행한다.

이때 실행 중인 Virtual Thread2가 park()되면 현재 실행 중인 정보와 스택은 Virtual Thread2의 Continuation에 저장되고 이 것은 jvm heap 영역에 저장된다.

이후 다음 차례를 기다리는 Virtual Thread1의 Continuation이 ForkJoinPool에 제출되면 위와 똑같은 과정으로 처리된다.

장점

  • 가상 스레드는 JVM에서 Continuation이라는 문맥 정보를 통해서 구현되기에 시스템 콜로 생성되는 기존 스레드보다 생성 비용과 문맥 교환 비용이 훨씬 적음
profile
문송 개발자

0개의 댓글