Computer Science Basics/운영체제

OS - 3. Thread and Concurrency

타자치는 문돌이 2024. 4. 22. 20:24

Process Model의 한계

CPU는 계속된 Context Switch를 통해 실행 중인 프로세스를 바꾸면서 여러 작업이 동시에 진행되는 것처럼 보이는 Concurrency를 달성했다.
그러나 이런 방식은 여러 개의 프로세스로 이루어진 작업에서 한계가 나타났는데,

  1. Context Switch 때마다 Text(Code), Data, Heap, Stack을 초기화하고 다시 로드해야 했고, 프로세스 통신을 통해 상태를 동기화해야 하는 자원 낭비가 일어났다.
  2. 그리고 여러 개의 CPU로 작동하는 Multi Processor 환경이 개발되었지만, 프로세스 하나를 돌릴 때는 Single Processor와 차이가 전혀 없었다.

이러한 문제를 해결하기 위해 Thread 개념이 도입되었다.


Thread란?

Thread는 프로세스의 제어 부분만 분리한 실행 단위이다.
프로세스는 최소 하나의 Thread를 가지고, 여러 개의 Thread(Multi Thread)를 가질 수도 있다.
프로세스를 제어 부분과 Code, Data, Heap의 자원 부분으로 분리한 다음, 자원 부분은 한 프로세스를 구성하는 여러 Thread가 공유하도록 한다.
이러면 한 프로세스의 Thread끼리 Context Switch 하는 비용이 줄어든다.

Thread는 CPU Scheduling의 기본 단위로 CPU는 한 번에 한 Thread를 실행한다.

프로세스의 실행과 관련해 4가지 상황이 있을 수 있다.

  1. 단일 Thread 프로세스 하나를 처리
  2. 단일 Thread 프로세스 여러 개를 처리
  3. Multi Thread 프로세스 하나를 처리
  4. Multi Thread 프로세스 여러 개를 처리
    그리고 이 Thread를 처리하는 CPU가 1개 또는 여러 개 있을 수 있다.
    여기서 우리는 3을 위주로 살펴볼 것이다.

Multi Threading

프로세스 하나가 Thread 여러 개를 사용하는 Multi Threading은 Parallelism과 Concurrency를 효율적으로 구현할 수 있다.

Parallelism (병렬성)

CPU 여러 개를 사용할 때 여러 Thread를 동시에 실행해 프로세스를 더 빨리 처리할 수 있다.

 


Concurrency (동시성)

CPU 한 개에서 빠른 Context Switch를 통해 여러 Thread를 동시에 처리하는 것 같은 Illusion을 만들 때, 자원 공유로 Context Switch의 속도가 더 빠르다.


장점

  • Concurrency와 Parallelism으로 더 효율적으로 프로세스를 처리한다.
  • I/O 대기 시간 동안 다른 Thread가 CPU를 사용할 수 있어 Throughput이 증가한다.
  • Multi Processor 구조에서 효율적이다.
  • 응답성이 높다.
  • 자원 공유를 프로세스 간 통신보다 효율적으로 한다.

주의

  • 모든 명령어가 CPU-Intense 하다면 비효율적이다.
  • Thread를 생성하는 게 저렴하긴 하지만, 무료는 아니다. 따라서 너무 단순한 코드를 Thread 여러 개로 처리하면 오히려 비효율적이다.

User-Level Thread와 Kernel-Level Thread

프로세스를 실행하면 Kernel은 Thread를 직접 생성/관리한다. 이런 방식을 Kernel-Level Thread라고 한다.

Kernel Level Thread 대신에 개발자가 Thread Library의 함수를 사용해 직접 Thread를 생성하고 관리할 수도 있다.
이걸 User Level Thread라고 한다. User Level Thread는 개발자가 생성한 Thread가 Kernel-Level Thread에 매핑되어 작동한다.


Mapping Model

User Level Thread와 Kernel의 Thread의 매핑 방식은 4가지 종류가 있다.

50

다대일

  • 여러 개의 User Level Thread에 하나의 Kernel Level Thread를 할당한다.
  • User Level Thread가 Block System Call을 호출하면 연결된 Kernel Level Thread는 막힌다.
  • Multi Processor에서 최적화된 모습은 아니다.
  • Concurrency는 가능하나 Parallelism은 불가능하다.

일대일

  • 하나의 User Level Thread에 하나의 Kernel Level Thread를 할당한다.
  • 윈도우, 리눅스는 이 방식을 사용한다.
  • 다대일의 Blocking 문제가 없다.
  • Multi Processor를 효율적으로 사용할 수 있다.
  • Kernel Level Thread가 많아지면 성능 문제가 발생한다.
  • Concurrency와 Parallelism이 가능하다.

다대다

  • 여러 개의 User Level Thread에 같거나 작은 수의 Kernel Level Thread를 할당한다.
  • 다대일의 Blocking 문제가 없다.
  • Multi Processor를 효율적으로 사용할 수 있다.
  • Concurrency와 Parallelism이 가능하다.
  • 구현이 까다롭다.

2 Level

  • 다대다의 변형
  • 상황에 따라 어떤 Thread는 일대일, 어떤 Thread는 다대다로 연결한다.
  • 유연하나 구현이 까다롭다.

비교

User Level Thread

  • 사용자의 Thread Library가 관리
  • Thread Library의 스케줄링 알고리즘을 사용
  • Thread 생성/소멸이 빠르다.
  • 구현이 수비다.
  • 가볍다.
  • 개발자가 자유롭게 설정할 수 있다.
  • 한 리소스에 여러 Thread가 접근할 때 발생하는 Race Condition이 발생하기 쉽다.
  • Scheduling 성능이 낮을 가능성이 높다.

Kernel Level Thread

  • OS의 Kernel이 관리
  • Kernel의 Scheduling Algorithm 사용
  • 강력한 동기화를 제공한다.
  • 직접 자원에 접근 가능하다.
  • 효율적인 스케줄링이 가능하다.
  • Thread의 생성/소멸이 느리다.
  • Overhead가 크다.
  • 구현이 복잡하다.

Thread의 이슈

fork()exec()

프로세스에서 fork를 사용하면, 실행 중인 프로세스를 복사해 Child Process를 생성한다고 했다. 이때 Thread는 fork를 실행한 Thread만 복사할까, 모든 Thread를 복사할까?
fork를 통해 프로세스를 복사해 실행하는 경우는 모든 Thread를 복사해야 한다.
한편, forkexec와 연계되어 프로세스를 복사하고 새 프로세스를 실행하는 경우가 많다. 이 경우 모든 Thread를 복사하는 것은 낭비이다.

이런 낭비를 막기 위해 몇몇 UNIX는 두 종류의 fork를 구현해 두었다.


Signal Handling

UNIX 시스템은 프로세스에 특정 이벤트가 일어났음을 알리기 위해 Signal을 사용한다.
A 프로세스가 A 프로세스에 알리는 경우 Synchronous Signal을 (Illegal Memory Access, Divide by 0 등),
다른 프로세스에 보내는 경우 Asynchronous Signal을(Ctrl+C를 통한 프로세스 종료, Timer 종료 등) 보낸다.

Single Thread Program에서는

  1. 특정 이벤트에 의해 Signal이 생성된다.
  2. Signal이 프로세스에 전달된다.
  3. Signal이 처리된다.
    의 과정으로 Signal을 처리한다.

Multi Thread Program에서는

  • Signal을 자기 자신에게 전달하거나
  • 프로세스의 모든 Thread에 전달하거나
  • 프로세스의 특정 Thread에 전달하거나
  • Thread 하나를 Signal 처리용으로 할당해 처리한다.

Thread Cancellation

Thread가 종료되기 전에 끝내야 하는 경우가 있다.
이때 두 가지 방법의 하나를 사용하는데,

  • Asynchronous Cancellation : 바로 Thread를 종료한다.
  • Deferred Cancellation : Thread가 종료되어도 괜찮은지 확인하고 종료한다.

Thread Pools와 Thread Local Storage

Thread Pool

Memory Pool처럼 미리 일정 개수의 Thread를 생성해 둔 다음, 필요한 경우 하나씩 꺼내서 쓰는 방식이다.
작업이 끝나면 Thread는 삭제되는 것이 아니라 대기 상태로 돌아가고,
새 작업이 시작되면 대기 중인 Thread를 꺼내와 사용한다.

이 경우 Thread 생성과 삭제의 Overhead가 줄어들고, 시스템 자원을 효과적으로 관리할 수 있다.

Thread Local Storage

메모리의 할당은 Thread 단위가 아니라 Process 단위로 이루어진다.
따라서 한 프로세스의 모든 Thread는 Data 영역을 공유하는데, 상황에 따라 Thread만의 공유 안 하는 전역변수가 필요한 경우가 있다.
이런 경우를 위해 Thread 별로 고유 영역을 제공하는데, 이를 Thread Local Storage라고 한다.

 

 

OS - 0. Index