14 minute read

프로세스의 문제점과 멀티스레딩의 필요성 (Motivaion)

현대 컴퓨터 시스템에서는 하나의 응용 프로그램이 다양한 작업을 동시에 처리해야 하는 요구가 많아지고 있다. 예를 들어, 웹 서버는 다수의 클라이언트 요청을 실시간으로 처리해야 하며, 워드 프로세서는 사용자의 키 입력을 감지하는 동시에 문법을 검사하고, 저장 작업도 함께 수행해야 한다. 이처럼 동시에 여러 작업을 수행해야 하는 상황이 점점 더 많아지고 있다.

기존의 구조에서는 이러한 작업을 각각 독립된 프로세스(Process)로 실행하여 처리했다. 하지만 이 방식에는 여러 가지 한계가 존재한다.

첫째, 문맥 교환(Context Switching) 비용이 크다. 하나의 프로세스에서 다른 프로세스로 실행을 전환하기 위해서는 CPU는 현재 실행 중인 프로세스의 상태를 저장하고, 다음에 실행할 프로세스의 상태를 복원해야 한다. 이 과정에서 많은 레지스터와 메모리 정보를 저장하고 불러오는 작업이 필요하므로, 전환 비용이 크고 성능 저하를 유발한다.

둘째, 프로세스 생성과 종료에 드는 비용이 크다. 새로운 작업을 처리하기 위해 새로운 프로세스를 생성해야 하는 경우, 커널은 해당 프로세스를 위한 별도의 주소 공간, 스택, 힙 등을 할당해야 하며 이는 시스템 자원을 많이 소비하게 만든다.

셋째, 프로세스 간 통신 및 동기화가 복잡하고 비용이 많이 든다. 프로세스는 기본적으로 독립적인 메모리 공간을 가지므로, 데이터를 주고받기 위해서는 IPC(Inter-Process Communication) 기법을 사용해야 한다. 이는 코드가 복잡해지고, 성능 측면에서도 오버헤드가 발생할 수 있다.

이러한 이유들 때문에, 여러 작업을 동시에 효율적으로 처리할 수 있는 대안으로 멀티 스레딩(Multithreading)이 도입되었다. 멀티스레딩은 하나의 프로세스 내부에서 여러 개의 실행 흐름(쓰레드)을 생성하여 작업을 병렬적으로 처리하는 방식이다. 각각의 쓰레드는 독립적으로 실행되지만, 프로세스의 자원(코드, 데이터, 힙 등)은 공유하므로 빠른 전환과 자원 공유가 가능하다.

멀티스레딩은 다음과 같은 장점을 제공한다.

  • 컨텍스트 스위칭 비용이 낮고 빠르다 : 쓰레드는 같은 프로세스 내부의 자원을 공유하기 때문에, 전환 시 메모리 맵이나 페이지 테이블을 다시 설정할 필요가 없다.
  • 쓰레드 생성/종료 비용이 적다 : 새로운 스레드는 프로세스의 주소 공간을 공유하기 때문에, 프로세스를 새로 생성하는 것보다 훨씬 가볍고 빠르다.
  • 자원 공유가 간단하다. : 스레드는 같은 주소 공간을 공유하므로, 프로세스 간 통신보다 훨씬 간단하게 데이터를 주고받을 수 있다.

이처럼 멀티스레딩은 프로그램의 응답성 향상, 자원 효율성 증가, 성능 최적화에 기여하며, 현대 운영체제와 응용 프로그램에서 필수적인 구조로 자리 잡게 되었다.

스레드 - Thread?

스레드(Thread)는 프로그램의 실행 흐름을 구성하는 가장 작은 단위로, CPU 스케줄링의 최소 단위이기도 하다. 전통적인 프로세스 구조에서는 하나의 프로세스가 하나의 실행 흐름만을 가지지만, 멀티스레딩이 적용된 구조에서는 하나의 프로세스가 여러 개의 스레드를 포함할 수 있다. 즉, 하나의 응용 프로그램 안에서 여러 작업을 동시에 수행할 수 있는 구조를 가능하게 한다.

Alt text

각 스레드는 독립적인 실행 흐름을 가지며, 다음과 같은 고유한 실행 상태를 유지한다.

  • 자신만의 스택(Stack) : 각 스레드는 함수 호출과 지역 변수 저장을 위한 독립적인 스택 공간을 가진다.
  • 프로그램 카운터(PC) : 현재 실행 중인 명령어의 위치를 나타낸다.
  • 레지스터 집합 : 연산에 사용되는 레지스터 상태가 각 스레드마다 별도로 유지된다.

이처럼 스레드는 자신만의 실행 정보를 갖고 있지만, 소속된 프로세스의 자원은 공유한다. 구체적으로는 다음과 같은 자원을 다른 스레드들과 함께 사용한다.

  • 코드(Code) : 실행할 프로그램의 명령어
  • 데이터(Data) : 전역 변수 등 공유되는 데이터
  • 힙(Heap) : 동적 메모리 영역
  • 파일 디스크립터(File Descriptor) : 열린 파일 및 네트워크 소켓 등

이러한 구조 덕분에 스레드는 프로세스보다 생성과 종료가 빠르고(very small creation time), 적은 메모리 사용(small memory occupation), 문맥 교환 비용(Context Switching Overhead)이 작으며, 자원 공유가 쉬워 동기화 비용(Synchronized overhead)도 상대작으로 낮다

Alt text

프로세스는 실행환경(자원)을 제공하는 컨테이너이며, 스레드는 그 안에서 실행되는 실행 단위이다. 하나의 프로세스는 여러 개의 스레드를 포함할 수 있으며, 이들 스레드는 자원을 공유하면서도 독립적으로 실행된다. 멀티스레드 구조는 자원 활용을 극대화하고, 성능과 응답성을 높이는데 매우 중요한 역할을 한다.

프로시저 호출(Procedure Call)과 멀티스레딩(Multithreading)의 비교

프로그래밍에서 여러 기능을 나누어 실행하는 기본적인 방법은 함수(Procedure) 호출이다. 그러나 보다 복잡한 프로그램이나 병렬 실행이 필요한 상황에서는 멀티스레딩(Multithreading)이 보다 효과적인 대안이 된다. 이 두 방식은 몇 가지 유사점과 중요한 차이점을 가진다.

공통점 : 메모리 구조와 데이터 접근

  • 함수 호출과 스레드는 모두 자신만의 지역 변수(Local variable)를 가진다. 즉, 한 함수(또는 스레드) 내부에서 선언된 변수는 다른 함수(또는 스레드)에서는 접근할 수 없다.
  • 반면에, 두 방식 모두 전역 변수(global variable)힙 영역(heap)에 있는 메모리에는 접근할 수 있다. 이러한 데이터는 프로그램 전체에서 공유되는 영역이기 때문이다.
  • 따라서 단순한 변수 사용 패턴에서는 이 둘은 비슷하게 작동한다. 하지만 중요한 차이점은 실행 방식과 동작의 흐름에 있다.

차이점1 : 실행 흐름의 독립성

  • 함수 호출순차적으로 실행되는 코드의 흐름을 따르며, 하나의 주 실행 흐름(main thread)에서 호출과 복귀가 이루어진다. 즉, 하나의 함수가 완료되기 전에는 다음 함수를 실행할 수 없다.
  • 반면, 스레드는 독립적인 실행 흐름을 가지며, 여러 스레드가 동시에(Cocurrently) 실행될 수 있다. 이를 통해 프로그램은 병렬적으로 여러 작업을 수행할 수 있다.
  • 예를 들어, 하나의 프로그램에서 Thread0이 파일을 읽는 동안 Thread1은 사용자 입력을 받는 작업을 동시에 수행할 수 있다. 이러한 동시성은 일반적인 함수 호출로는 구현하기 어렵다.

차이점2 : 스택 구조

  • 각 함수 호출공유된 스택 프레임(Stack Frame) 위에 자신의 공간을 할당받아 실행된다. 따라서 모든 함수는 하나의 공통 스택을 공유한다. (동일한 스택 공간에서 발생)
  • 각 스레드는 자신만의 독립된 스레드를 가진다. 이를 통해 스레드 간 지역 변수의 충돌을 방지하고, 서로의 실행 흐름에 영향을 주지 않고 병렬로 동작할 수 있다. (분리된 스택 공간에서 발생)
    • 하나의 프로세스에는 하나의 가상 주소 공간이 있고, 그 안에서 스레드는 자신의 스택을 따로 할당 받는다. 즉, ‘스택 영역’은 전체적으로는 프로세스 주소 공간에 포함되지만, 실제로는 스레드마다 분리된 스택을 운영체제가 별도로 할당을 해주는 것이다.

차이점3 : 병렬성

  • 함수 호출은 CPU가 순차적으로 명령어를 실행하기 때문에 병렬성이 없다.
  • 멀티스레딩은 멀티코어 CPU와 함께 사용할 경우 진정한 병렬 실행이 가능하다. 각 코어가 다른 스레드를 실행함으로써 전체 프로그램의 처리 속도를 크게 향샹시킬 수 있다.

단일 스레드 vs 멀티 스레드

운영체제에서 실행되는 프로그램은 기본적으로 프로세스(Process) 단위로 관리된다. 하지만 각 프로세스가 하나의 실행 흐름만을 가지는지, 아니면 여러 실행 흐름(스레드)를 동시에 가지는지에 따라 그 구조와 동작 방식은 크게 달라진다. 이 개념을 이해하기 위해서는 싱글 스레드(Single Thread) 프로세스와 멀티 스레드(Multi Thread) 프로세스를 구분해서 살표볼 필요가 있다.

싱글 스레드 프로세스

싱글 스레드 프로세스는 하나의 프로세스에 단 하나의 실행 흐름(thread)만 존재하는 구조이다. 프로그램이 시작되면 하나의 스레드가 생성되어, 해당 스레드가 모든 작업을 순차적으로 처리하게 된다.

이 구조는 설계가 단순하고 구현이 쉬우며, 동기화나 자원 충돌에 대한 고민이 적다는 장점이 있다. 하지만 다음과 같은 한계도 존재한다.

  • 하나의 작업이 블로킹(예: 디스크 I/O, 네트워크 대기 등)되면 프로세스 전체가 멈춘다.
  • 여러 작업을 동시에 처리하는 것이 불가능하므로, 응답성과 성능이 떨어진다.

예를 들어, 싱글 스레드 기반의 웹 서버는 한 클라이언트의 요청을 처리하는 동안 다른 요청을 받지 못해 병목 현상이 발생할 수 있다.

멀티 스레드 프로세스

멀티 스레드 프로세스는 하나의 프로세스 안에 여러 개의 실행 흐름(스레드)이 존재하는 구조이다. 이 구조에서는 여러 스레드가 동시에 동작하면서 각가그이 작업을 수행할 수 있다.

멀티 스레드는 자원을 공유하면서도 독립적인 실행 흐름을 가지는 구조이기 때문에 다음과 같은 장점이 있다.

  • I/O 대기 작업으로 인해 하나의 스레드가 멈춰도, 다른 스레드가 계속 실행되어 전체 프로그램이 멈추지 않는다.
  • 사용자에게 보다 빠른 응답을 제공할 수 있으며, 멀티코어 CPU 환경에서 병렬성(Parallelism)을 극대화할 수 있다.
  • 하나의 프로세스 내에서 스레드 간 자원 공유가 용이하여 프로세스 간 통신(IPC)에 비해 효율적이다.

예를 들어, 멀티 스레드 기반 웹 서버는 클라이언트 요청마다 새로운 스레드를 생성하거나 스레드 풀에서 할당하여 동시에 여러 요청을 처리할 수 있다.

Alt text

스레드 생성 방법과 스레드 라이브러리

멀티스레딩을 구현하기 위해서는 프로그래머가 직접 스레드를 생성하고 제어할 수 있는 도구, 즉 스레드 라이브러리(thread Library)가 필요하다. 운영체제는 이러한 라이브러리를 통해 스레드 생성, 종료, 동기화 등의 기능을 제공하며, 프로그래머는 이를 사용해 병렬성과 효율성을 극대화할 수 있다.

스레드 라이브러리(Thread Library)

스레드 라이브러리는, 프로그래머가 스레드를 생성하고 관리할 수 있도록 도와주는 API 집합이다. 이를 통해 다음과 같은 작업이 가능하다.

  • 스레드 생성(create)
  • 스레드 종료(exit)
  • 스레드 간 동기화(mutex, condition, variable 등)
  • 스레드 스케줄링 및 제어(우선 순위, 대기 등)

스레드 라이브러리는 내부 구현 방식에 다라 크게 사용자 수준(User-level)커널 수준(Kernel-level)으로 나뉜다.

  • 사용자 수준 스레드(User-Level Treads, UTL)
    • 운영체제 커널의 관여 없이, 사용자 공간(User space)에서 스레드를 관리한다.
    • 라이브러리 내부에서 스레드를 생성하고 스케줄링하므로, 시스템 콜이 필요하지 않아 속도가 빠르다.
    • 장점 : 빠른 컨텍스트 스위칭, 높은 휴대성(Portability)
    • 단점 : 하나의 스레드가 시스템 콜로 블로킹되면 프로세스 전체가 멈춘다. 또한 커널은 프로세스 내 스레드의 존재를 모른다. -> CPU 병렬 실행에 제한이 있다.
  • 커널 수준 스레드(Kernel-Level Threads, KLT)
    • 스레드의 생성, 스케줄링, 동기화 등이 모두 커널 수준에서 처리된다.
    • 각 스레드는 커널이 인식하며, 스케줄러에 의해 개별적으로 CPU에 할당된다.
    • 장점 : 하나의 스레드가 블로킹되더라도, 다른 스레드는 계속 실행 가능하다.(진정한 병렬 실행(in multicore))
    • 단점 : 시스템 콜을 동반하기 때문에 사용자 수준보다 컨텍스트 스위칭 비용이 크다.

스레드 모델(Multithreading Models)

멀티스레딩을 운영체제 수준에서 어떻게 구현하느냐에 따라 스레드 모델(Thread model)은 여러 가지로 구분된다. 이 모델은 사용자 수준 스레드(User-Level Thread, ULR)커널 수준 스레드(Kernel-Level Thread, KLT) 간의 매핑 방식에 따라 다음 네 가지 형태로 나눌 수 있다.

  • Many-to-One(M:1) 모델
  • One-to-Ont(1:1) 모델
  • Many-to-Many(M:N) 모델
  • Two-Level 모델

이들 모델은 각각 장단점이 있으며, 운영체제마다 선택하는 방식이 다르다.

Many-to-One(M:1) 모델

Many-to-One 모델여러 개의 사용자 수준 스레드(ULT)하나의 커널 스레드(KLT)에 매핑되는 구조이다. 즉, 프로세스 내의 모든 스레드가 단 하나의 커널 스레드를 공유하면서 실행된다.

이 모델에서 모든 스레드의 생성과 스케줄링은 사용자 공간에서 이루어진다. 커널은 스레드의 존재를 인지하지 못하기 때문에, 프로세스 전체를 하나의 단일 실행 흐름으로 다루게 된다.

Alt text

  • 구조 및 동작 흐름
    1. 프로세스가 시작되면, 사용자 공간에 있는 스레드 라이브러리가 작동하며 스레드를 생성하고 관리한다.
    2. 여러 개의 사용자 스레드가 존재하더라도, 이들은 하나의 커널 스레드를 공유하며 번갈아 실행된다.
    3. 어떤 스레드가 실행 중이라면, 다른 스레드는 해당 스레드가 CPU를 내놓을 때 까지 대기해야 한다.
    4. 중요한 점은, 스케줄링도 커널이 아닌 사용자 공간에서 처리된다는 것이다. 이는 시스템 콜 없이 빠르게 스레드를 전환할 수 있다는 의미이다.

블로킹 문제의 발생 : 이 모델에서 가장 큰 단점이자 구조적인 한계는 하나의 스레드가 시스템 호출(예: I/O 요청)로 블로킹될 경우 전체프로세스가 정지한다는 점이다.

예를 들어, 다음과 같은 상황을 가정해보자:

  • 사용자 스레드 A는 파일에서 데이터를 읽기 위해 read() 시스템 콜을 호출한다.
  • 이때 커널 스레드는 I/O 작업이 완료될 때까지 블로킹 상태가 된다.
  • 문제는, 스레드 A만 멈추는 것이 아니라, 같은 커널 스레드를 공유하는 스레드 B, C 등 모든 사용자 스레드들도 함께 멈춘다는 것이다.

이로 인해 프로세스 전체가 CPU를 사용하지 못하고 놀게 되는 비효율적인 상황이 발생할 수 있다.

예시 : Solaris Green Thread, GNU Portable Thread

Many-to-One 모델은 구현이 간단하고 스레드 전환이 빠르다는 장점이 있지만, 현대의 멀티코어 시스템이나 I/O 중심 애플리케이션에는 부적합하다. 병렬성과 실시간 응답성을 중시하는 현대 애플리케이션에서는 One-to-One이나 Many-to-Many 구조가 선호된다. 그러나 단순한 작업이나 제한된 환경에서는 여전히 효율적인 선택이 될 수 있다.

One-to-One(1:1) 모델

One-to-One(1:1) 스레드 모델각 사용자 수준 스레드(User-Level Thread)하나의 커널 수준 스레드(Kernel-Level-Thread)에 각각 매핑되는 구조이다. 즉, 스레드를 하나 생성하면, 운영체제 커널은 그 스레드를 직접 실행 가능한 커널 스레드로 등록하고 관리한다.

이 구조에서는 커널이 각 스레드를 독립적인 실행 단위로 인식하며, 스케줄링, 시스템 콜 처리, 동기화 등도 모두 커널 수준에서 수행된다. 이를 통해 병렬성, 안정성, 응답성 측면에서 매우 강력한 구조를 제공한다.

Alt text

  • 구조 및 동작 흐름
    1. 프로세스가 실행되고 나면, main() 함수가 포함된 기본 스레드가 커널 스레드로 등록된다.
    2. 프로그래머가 새로운 스레드를 생성하면, 운영체제는 사용자 스레드와 1:1로 매핑되는 새로운 커널 스레드를 함께 생성한다.
    3. 각 스레드는 커널 수준에서 독립적으로 스케줄링되며, 멀티코어 환경이라면 서로 다른 CPU 코어에서 병렬적으로 실행될 수 있다.
    4. 하나의 스레드가 시스템 콜로 인해 블로킹되더라도, 다른 스레드는 영향을 받지 않고 실행된다.

병렬 실행과 멀티코어 활용 : One-to-One 모델의 가장 큰 장점은 진정한 병렬성(real parallelism)을 구현할 수 있다는 것이다.

  • 커널이 각각의 스레드를 독립적으로 인식하기 때문에, 멀티코어 CPU 환경에서는 여러 스레드가 동시에 실행될 수 있다.
  • 예를 들어, 스레드 A는 CPU 0에서 파일을 읽고, 스레드 B는 CPU 1에서 계산 작업을 동시에 수행할 수 있다.
  • 이 구조는 CPU 자원을 최대한 활용하면서도, 스레드 간의 병목 없이 고성능 실행을 가능하게 한다.

하지만 이 모델에도 단점은 존재한다. 스레드를 생성할 때마다 커널에서도 새로운 스레드를 생성해야 하므로, 많은 스레드를 사용하는 프로그램에서는 커널 리스소를 과도하게 소모할 수 있다.

  • 수천 개의 스레드를 생성할 경우, 각 스레드마다 스택, 커널, 구조체, PCB 등이 생성되기 때문에 시스템 전체에 부담을 줄 수 있다.
  • 또한, 커널 수준 컨텍스트 스위칭은 사용자 수준보다 느리기 때문에, 스레드가 너무 많아지면 전환 오버헤드가 증가한다.

예시 : Linux(NPTL: Native POSIX Thread Library), macOS, Solaris 9 이후 버전, Windows NT/2000/XP/10

One-to-One 모델은 고성능 서버, 병렬 계산, 실시간 반응이 필요한 애플리케이션에서 이상적인 선택이다. 멀티코어를 적극 활용하고, 스레드 간 독립성을 보장해야 하는 프로그램에서는 이 모델이 안정성과 속도를 동시에 제공한다.

다만, 너무 많은 수의 스레드가 필요한 프로그램이라면, 자원 효율성 측면에서 Many-to-Many나 스레드 풀 구조와의 병행 사용이 고려될 수 있다.

Many-to-Many(M:N) 모델

Many-to-Many(M:N) 스레드 모델여러 개의 사용자 수준 스레드(User-Level Threads)적거나 같은 수의 커널 수준 스레드(Kernel-Level-Threads)에 매핑되는 구조이다. 이 모델은 Many-to-OneOne-to-One 모델의 장점을 절충한 형태로, 병렬성과 유연한 스레드 관리를 동시에 추구한다.

즉, 프로세스 안에서 많은 사용자 스레드가 존재할 수 있지만, 커널에 등록되는 스레드 수는 제한되어 있으며, 이 스레드들 위에서 사용자 스레드들이 효율적으로 스케줄링된다.

Alt text

  • 구조 및 동작 흐름
    1. 애플리케이션은 필요에 따라 많은 사용자 스레드를 생성할 수 있다.
    2. 이 사용자 스레드들은 소수의 커널 스레드에 분산(multiplexing)되어 실행된다.
    3. 사용자 스레드 간의 전환은 사용자 수준에서 빠르게 일어나고, 커널 스레드는 동시에 여러 사용자 스레드를 처리할 수 있도록 운영체제가 관리한다.
    4. 멀티코어 환경에서는 커널 스레드들이 각기 다른 CPU 코어에 배정되므로 진정한 병렬 실행도 가능하다.

블로킹 문제 해결 : Many-to-Many 모델은 Many-to-one 모델의 가장 큰 단점, 즉 한 스레드의 블로킹이 전체를 멈추게하는 문제를 해결한다.

  • 사용자 스레드 A가 디스크 I/O로 블로킹되더라도,
  • 다른 커널 스레드에 할당된 사용자 스레드 B, C는 계속 실행된다.
  • 이렇게 병렬성과 안정성이 확보하면서도, 사용자 스레드의 유연한 생성과 관리가 가능하다.

이 모델의 강점은, 커널 스레드 수에 비해 훨씬 많은 사용자 스레드를 사용할 수 있다는 점이다. 시스템이 허용하는 커널 스레드의 수가 제한적일 때에도, 애플리케이션은 필요한 만큼의 스레드를 사용자 공간에서 만들고 관리할 수 있다.

  • 커널이 담당하는 스레드 수를 제한하여 시스템 자원 낭비를 줄일 수 있으며,
  • 사용자 스레드의 스케줄링은 가볍게 처리되어 빠른 실행과 응답성을 제공한다.

예시 : 초기 Solaris 시스템, Windows NT

Many-to-Many 모델은 고성능 서버, 대규모 이벤트 기반 시스템, 제한된 커널 자원 환경에서 매우 유용하다. 사용자 스레드를 많이 사용하되, 시스템 자원을 효율적으로 제어하고 싶은 경우에 적합하다. 하지만 구현이 복잡하고, 성능의 일관성을 유지하려면 스케줄링 알고리즘과 자원 분배 전략이 정교해야 하므로 일반적인 응용에서느느 자주 사용되지 않는다.

Two-Level 모델

Two-Level 스레드 모델은 이름 그대로 스레드 구조를 두 개의 계층(Two Levels)으로 나누어 관리하는 방식이다. 이 모델은 Many-to-Many 모델(M:N)을 기반으로 하면서, 일부 스레드를 특정 커널 스레드에 1:1로 바인딩할 수 있는 기능까지 제공한다.

즉, 다수의 사용자 수준 스레드(User-Level Threads)적은 수의 커널 수준 스레드(Kernel-Level-Threads)에 매핑하면서, 동시에 필요한 스레드는 커널 스레드에 직접 연결(1:1)하여 우선 실행이나 예외적 처리를 가능하게 한다.

Alt text

  • 구조 및 동작 흐름
    1. 많은 사용자 스레드는 소수의 커널 스레드 위에서 실행된닫.
    2. 사용자 스레드 간의 전환은 사용자 공간에서 관리되며, 커널 스레드는 병렬 실행을 지원한다.
    3. 그런데 여기서 차별화된 점은, 프로그래머가 일부 사용자 스레드를 특정 커널 스레드에 직접 바인딩(Bind)할 수 있다는 것이다.

예를 들어, 어떤 스레드는 응답성이 매우 중요하거나 실시간 처리가 필요한 경우, 운영체제가 스케줄링하기를 기다리지 않고 즉시 실행되도록 보장해야 할 수 있다. 이런 경우 Two-Level 모델에서는 해당 스레드를 커널 스레드에 직접 연결해, 다른 스레드와 구분하여 우선 순위적으로 실행할 수 있다.

Many-to-Many 모델은 스레드와 커널 스레드 간의 매핑이 유동적이지만, 어떤 스레드를 특정한 시점에서 강제로 우선 실행시키거나, 특수 목적의 자원과 직접 연결시켜야 할 때 명확한 제어가 어렵다.

Two-Level 모델은 이러한 문제를 해결하고자 등장한 모델이다. 필요할 경우 커널 수준의 정밀한 제어를 제공하면서도, 그 외의 스레드는 효율적인 사용자 수준에서 관리할 수 있게 한다.

이 모델의 가장 큰 장점은 유연성(Flexibility)성능(Performance)동시에 확보할 수 있다는 점이다.

  • 성능 면에서는 사용자 스레드를 커널 스레드보다 훨씬 많이 사용할 수 있으며, 필요한 시점에 빠르게 생성, 삭제가 가능하다.
  • 제어 측면에서는, 커널 스레드와의직접 연결을 통해 특수한 처리나 고우선 작업을 안정적으로 실행할 수 있다. 이러한 특성은 특히 멀티코어 서버, 실시간 시스템, OS 레벨 제어가 필요한 작업에서 매우 유용하다.

예시: Solaris 8 이하 버전, IRIX, HP-UX, Tru64 UNIX…

Two-Level 모델유연한 사용자 수준 스레드 관리정밀한 커널 제어가 모두 필요한 경우에 가장 적합하다. 예를 들어, 실시간 처리를 일부 보장하면서도, 나머지 작업은 효율적으로 병렬 처리해야 하는 시스템에서는 최적의 선택이 될 수 있다.

하지만 현대 운영체제에서는 구조 단순화와 유지 보수를 위해 대부분 One-to-One으로 수렴하고 있으며, Two-Level 구조는 일부 특수 목적 시스템이나 고성능 응용에서만 채택된다.

Thread Cancellation

멀티스레딩 환경에서는 때로는 진행 중인 스레드를 강제로 종료시켜야 할 필요가 있다. 예를 들어, 긴 시간 동안 응답하지 않는 스레드나, 사용자가 취소한 작업을 수행 중인 스레드가 그 대상이 될 수 있다. 이때 사용하는 개념이 바로 스레드 취소(Thread Cancellation)이다.

스레드 취소(Thread Cancellation)는 특정 스레드를 명시적으로 종료시키는 행위로, 아직 완료되지 않은 스레드의 실행을 강제로 중단시키는 것을 의미한다. 이를 통해 불필요한 자원 소모를 방지하고, 시스템의 응답성을 향상시킬 수 있다.

취소 방식에는 두 가지 방식이 있다.

  • 비동기 취소(Asynchronous Cancellation)
    • 즉시 취소 요청을 보내고 스레드를 강제로 종료시킨다.
    • 단순하지만, 데이터 일관성을 해칠 위험이 크다.
    • 예: 락을 보유한 상태에서 강제 종료되면, 다른 스레드가 영원히 락을 기다릴 수 있다.
  • 지연 취소(Deferred Cancellation)
    • 취소 요청은 먼저 보내되, 스레드 내부에서 “취소 가능 지점(cancellation point)”에 도달할 때까지 기다린 후 종료된다.
    • 스레드가 스스로 점검하고 안전하게 종료할 수 있으므로, 자원 정리와 동기화 문제가 줄어든다.
  • 취소의 문제점 : 공유 자원 락 : 스레드 취소에서 가장 큰 어려움은 공유 자원과 락이다. 예를 들어, 어떤 스레드가 락을 획득하고 아직 반납하지 않은 상태에서 강제 종료되면, 그 락을 기다리던 다른 스레드는 영원히 대기 상태에 빠질 수 있다.(deadlock)

Alt text

  • 예제:
Thread 0:
    lock();             // 락 획득
    globalVar = 200;
    unlock();

Thread 1:
    lock();             // 락이 해제될 때까지 대기 중...
    globalVar = 200;
    unlock();
  • Thread0 이 lock() 이후 강제로 종료되면, Thread1은 락을 절대 획득하지 못하고 대기만 하게 된다.
    • Linux에서는 Thread1이 영원히 sleep 상태
    • Windows에서는 커널이 해당 락을 회수하고 Thread1이 깨어날 수 있도록 설계되어 있다.
    • 이러한 문제 때문에, 대부분의 시스템은 지연 취소를 기본 방식으로 채택하고 있다.

Thread Pool

멀티스레드 시스템에서는 요청마다 스레드를 새로 생성하는 방식은 성능 면에서 비효율적일 수 있다. 특히 서버나 GUI 앱처럼 수많은 요청을 빠르게 처리해야 하는 시스템에서는 스레드를 재사용할 수 있는 구조가 필요하다. 이때 사용되는 것이 바로 스레드 풀(Thread Pool)

스레드 풀(Thread Pool)은 미리 일정 개수의 스레드를 생성해 두고, 요청이 들어올 때마다 이들 중 하나를 할당해 처리하는 방식이다. 작업이 끝난 스레드는 종료되지 않고 풀에 반환디어 재사용된다.

Alt text

  • 장점
  • 스레드 생성/소멸 오버헤드 감소 : 매번 스레드를 생성하지 않아도 되므로 성능이 향상된다.
  • 자원 관리 효율적 : 동시에 실행될 스레드 수를 제한할 수 있어, 과도한 CPU/메모리 사용을 방지할 수 있다.
  • 응답 속도 향상 : 이미 만들어진 스레드를 즉시 활용할 수 있어 처리 시간이 빨라진다.

Pthreads

Pthreads는 POSIX(Portable Operating System Interface for UNIX) 표준 중 하나인 IEEE 1003.1c에 정의된 쓰레드 생성 및 동기화를 위한 API이다. 이 API는 쓰레드 라이브러리의 동작 방식만을 규정하며, 실제 구현은 각 운영체제 또는 라이브러리 개발자에게 달려 있다.

Pthreads는 주로 UNIX 계열 운영체제에서 널리 사용되며, 대표적으로 Solaris, Linux, macOS 등에서 지원된다. 해당 API를 사용하면 플랫폼에 상관없이 쓰레드를 생성하고 관리할 수 있으며, 다양한 동기화 메커니즘도 함께 제공된다.

Window XP Threads

Windows XPOne-to-One 매핑 방식의 스레드 모델을 구현하고 있다. 즉, 하나의 사용자 스레드는 하나의 커널 스레드에 직접 매핑된다. Windows의 각 스레드는 다음과 같은 구성 요소를 가진다.

  • 스레드 ID(Thread ID)
  • 레지스터 집합(Register set)
  • 사용자 스택과 커널 스택(User and Kernel Stack)
  • 스레드 전용 데이터 저장 영역(Private data storage area)

이러한 구성 요소들, 즉 레지스터 집합, 스택들, 데이터 저장 영역은 통틀어 스레드 컨텍스트(Context)라고 불리며, 스레드간 전환 시 이 컨텍스트가 저장되고 복원된다.

Windows XP에서 스레드를 표현하는 주요 데이터 구조로는 다음과 같은 것들이 있다.

  • ETHREAD(Executive Thread Block)
  • KTHREAD(Kernel Thread Block)
  • TEB(Thread Enviroment Block) 이 구조체들은 각각의 스레드 상태를 저장하고, 커널과 사용자 모드 간의 연동을 가능하게 한다.

Linx Threads

Linux에서는 쓰레드를 쓰레드(thread)라고 부르기보다는 태스크(task)라는 용어를 사용한다. 쓰레드의 생성은 일반적으로 clone() 시스템 호출을 통해 수행된다.

이 clone() 함수는 부모 태스크(프로세스)의 주소 공간을 자식과 공유할 수 있도록 허용한다. 이로 인해 새로운 태스크는 기존 프로세스의 메모리 공간을 공유하면서 효율적인 쓰레드 형태로 동작할 수 있게 된다.

즉, Linux는 clone()을 이용한 태스크 복제 메커니즘을 통해 쓰레드를 구현하고 있으며, 이를 통해 쓰레드 간 빠른 통신과 자원 공유가 가능해진다.

Tags: ,

Categories:

Updated: