[OS] 스레드 (Threads)
[OS] 스레드 (Threads)

[OS] 스레드 (Threads)

카테고리
💻 Computer Science
작성자
박용성박용성
작성일
2024년 06월 03일
태그
OS
floatFirstTOC: right

🖥️ 시작하며

왜 스레드를 사용하는지, 스레드를 사용하는 데 있어 문제는 무엇인지에 대해 알아보려고 합니다.
 

🔍 프로세스의 문제점

💡
결국은, 너무 무겁다.
  • 프로세스의 구성 요소:
    • 주소 공간: 모든 코드와 데이터 페이지를 포함합니다.
    • OS 자원: 열린 파일 등과 계정 정보가 포함됩니다.
    • 하드웨어 실행 상태: 프로그램 카운터(PC), 스택 포인터(SP), 레지스터 등을 포함합니다.
  • 새로운 프로세스를 생성하는 것은 모든 데이터 구조를 할당하고 초기화해야 하기 때문에 매우 비용이 많이 듭니다.
    • 예: Linux에서는 task_struct에 100개 이상의 필드가 있습니다.
  • 프로세스 간 통신은 주로 OS를 통해 이루어져야 하기 때문에 비용이 많이 듭니다.
    • 시스템 호출 및 데이터 복사의 오버헤드가 발생합니다.
 

📌 개선된 프로세스 관리 방법

  • 공간 문제:
    • PCB, 페이지 테이블 등의 자원 소모가 큽니다.
  • 시간 문제:
    • OS 구조를 생성하고, fork 및 주소 공간을 복사하는 데 많은 시간이 소요됩니다.
  • 해결 방법:
    • 여러 프로세스를 병렬로 실행해야 합니다.
    • 각 프로세스가 동일한 주소 공간을 매핑하여 데이터를 공유해야 합니다.
      • 예: 공유 메모리
    • OS가 이러한 프로세스를 병렬로 스케줄링해야 합니다.
 
유사점
차이점
주소 공간: 동일한 코드와 데이터를 사용합니다.
하드웨어 실행 상태: 각 프로세스는 고유의 PC, 레지스터, SP 및 스택을 가집니다.
권한: 동일한 권한을 사용합니다.
자원: 파일, 소켓 등 동일한 자원을 사용합니다.
→ 결국 실행 상태를 분리하면 됩니다.
 
notion image
 

🔍 스레드

💡
프로그램 내에서 실행되는 명령어의 연속적인 흐름, 하드웨어의 수행 상태
  • 구성 요소:
    • 프로그램 카운터 및 일반 레지스터
    • 지역 변수와 반환 주소를 추적하기 위한 스택
  • 스레드는 프로세스의 명령어와 대부분의 데이터를 공유합니다.
    • 하나의 스레드가 공유 데이터를 변경하면, 다른 스레드도 그 변경사항을 볼 수 있습니다.
  • 스레드는 또한 프로세스의 대부분의 OS 상태를 공유합니다. → 자원 공유!
 

📌 멀티스레드의 이점

  • 병행성 생성 비용이 적음: 스레드는 프로세스를 생성하는 것보다 적은 시간과 메모리를 소모합니다.
  • 프로그램 구조 개선: 스레드를 사용하면 코드 구조가 더 나아지고, 복잡한 작업을 분리하여 관리하기 쉬워집니다.
  • 높은 처리량: 스레드 간에 작업을 분산시켜 I/O 작업과 계산 작업을 동시에 수행할 수 있어 처리량이 증가합니다.
  • 더 나은 응답성: 사용자 인터페이스와 서버 응답성이 향상됩니다. 예를 들어, 웹 서버는 여러 요청을 동시에 처리할 수 있습니다.
  • 자원 공유 용이: 스레드는 동일한 주소 공간을 공유하므로 자원 관리가 효율적입니다.
  • 다중 프로세서 아키텍처 활용: 스레드를 통해 병렬 처리가 가능해지며, 멀티프로세서 시스템에서 성능을 극대화할 수 있습니다.
 
프로세스 (Process)
스레드 (Thread)
연결 관계
하나 이상의 스레드를 포함할 수 있음
단일 프로세스에 종속됨
데이터 공유 비용
프로세스 간 데이터 공유는 비용이 큼
스레드 간 데이터 공유는 저렴함
주소 공간
각 프로세스는 고유한 주소 공간을 가짐
동일한 주소 공간을 공유
스케줄링 단위
전통적으로 프로세스가 스케줄링 단위였음
스레드가 스케줄링 단위가 됨
실행 환경
프로세스는 스레드가 실행되는 컨테이너 역할을 함
프로세스 내에서 실행됨
자원 관리
프로세스마다 독립적으로 자원을 관리
자원 관리는 프로세스 수준에서 이루어짐
오버헤드
프로세스 생성과 전환 시 오버헤드가 큼
스레드 생성과 전환 시 오버헤드가 적음
보안 및 안정성
각 프로세스는 서로 독립적이어서 보안 및 안정성이 높음
스레드 간의 충돌이 있을 수 있으며 보안에 취약할 수 있음
notion image
notion image
notion image
notion image
 

🔍 스레드 이슈

📌 fork() 호출 시 문제점

  • 새 프로세스가 모든 스레드를 복제하는가?
  • 새 프로세스가 단일 스레드인가?

📌 UNIX 시스템의 두 가지 fork() 버전

  • pthreads에서:
    • fork()는 호출한 스레드만 복제합니다.
  • Unix에서:
    • fork()는 부모 프로세스의 모든 스레드를 자식 프로세스에서 복제합니다.
    • fork1()은 호출한 스레드만 복제합니다.

💡 exec()는 일반적으로 주소 공간을 날려버리므로 스레드도 날라갑니다.

📌 exit() 호출 시 문제점

  • 스레드가 exit()를 호출하면?
    • 프로세스 내의 모든 스레드를 종료시킵니다.
  • 메인 스레드가 자식 스레드보다 먼저 종료(return, exit())하면?
    • 대부분의 경우, 메인 스레드가 종료되면 전체 프로세스가 종료되므로 자식 스레드도 함께 종료됩니다.
      • → 멀티 스레드는 exit로 종료하면 안 됩니다. 무조건 return으로 종료해야 합니다.
 
취소 방식
설명
문제점 및 고려사항
비동기 취소 (Asynchronous Cancellation)
- 대상 스레드를 즉시 종료합니다.
- 대상 스레드가 자원을 보유하고 있거나, 공유 자원을 업데이트 중인 경우 문제가 발생할 수 있습니다.
지연 취소 (Deferred Cancellation)
- 대상 스레드는 취소 지점에서 종료됩니다.
- 대상 스레드는 주기적으로 자신이 취소되어야 하는지 확인해야 합니다.

💡 Signal handling의 문제

운영체제가 시그널을 보낼 때, 기본적으로 커널이 프로세스에게 보냅니다.
  • 스레드를 사용할 때 어떤 스레드가 시그널을 받아야 하는지 정해야 합니다.
    • 모든 스레드
    • bit mask 를 사용해 특정 스레드가 시그널을 받을지
 

⚙️ 라이브러리는 내부에 독립적인 버전의 변수를 가질 수 있습니다. (ex: errno)

notion image
 

📌 Multithread-safe (MT-safe)

💡
라이브러리에 이 라이브러리가 멀티스레드에서 안전한지 명시되어 있음
전역 변수를 수정하는 경우 멀티스레드에서 안전한 상태로 만들어야 합니다. 읽기만 하면 괜찮습니다.
 

지금까지의 스레드는 커널 스레드에 대해 이야기했습니다. 지금부터는 유저 레벨 스레드에 대해 이야기합니다.
 

🔍 가장 빠른 함수는?

Local Function
User-Level Thread
System call
Kernel-level Thread
속도
가장 빠름
빠름
느림
매우 느림
이유
Local Stack에서 해결 가능
OS 관여 없이 라이브러리에서 처리
일반적으로 사용자 모드에서 커널 모드로, 그리고 다시 사용자 모드로의 단일 전환을 포함
커널로 컨텍스트 스위칭이 필요하므로 오버헤드가 심함, 스케줄링과 동기화도 필요함 커널 수준 스레드는 커널이 관리해야 할 추가적인 자원과 상태 정보를 필요
 

📌 Kernel-level Threads VS User-level Threads

Kernel-level
User-level
생성/관리
System call → 오버헤드 증가
local 함수나 라이브러리
특징
OS 수준에서 스레드를 알 수 있음
OS는 스레드를 볼 수 없음
스케줄링
OS가 가능
라이브러리가 수행
병렬성
OS 수준에서 스케줄링, 컨텍스트 스위칭 수행하므로 병렬성 좋음
OS가 직접 스레드를 스케줄링 할 수 없으므로 병렬성 낮음
비용
매우 고비용 (커널 모드 전환)
상대적 저비용
리소스 관리
커널에서 스레드 조각 생성 제한 (25만개로 제한)
이론적 제한 X
  • 유저 레벨 스레드가 가능한 이유
    • 같은 주소 공간을 공유함
    • 스레드끼리는 대략 Hardware context만 상이함
      • → 사용자 수준 프로세스 자체에서 조작 가능
 

📌 User-level Threads

  • 더 싸고, 빠른 스레드가 필요했음
  • User-level Threads는 런타임 시스템(사용자 수준 라이브러리)에서 전적으로 관리해서 이식성이 좋음
  • 각 스레드는 Hardware context와 작은 TCB를 가짐
  • 스레드 생성, 전환, 동기화는 프로시저 호출을 통해 수행됨 (커널 개입 없음)
    • 💡
      프로시저 호출은 프로그램의 흐름을 특정 작업을 수행하는 코드 블록으로 전달하는 것입니다. 이 과정에서 현재의 실행 상태(예: 프로그램 카운터, 레지스터 값 등)를 저장하고, 호출된 프로시저(함수, 메서드)로 제어를 넘긴 다음, 해당 프로시저가 실행을 완료하면 원래의 실행 위치로 돌아오는 과정을 포함합니다.
notion image

⚙️ User-level Thread의 Context Switch

  • 커널이 할 일을 라이브러리가 하므로 매우 간단함
  • 각 스레드는 TCB의 일부로써 스택을 가지고, push로 저장하고 pop으로 상태 복원 수행
notion image
 

📌 User-level Threads의 한계

  • User-level Threads는 OS에게서 보이지 않으므로 context switch시 좋지 않은 결정을 내릴 수 있음
      1. I/O 중인데 CPU 할당
      1. I/O 시작한 스레드를 Block
      1. 뺏으면 안되는 상황에 뺏음
 
🙋 다른 상황에서는 프로세스의 instruction이 사용 중이라도, Timer 하드웨어를 통해 interrupt를 주어 context switch를 하게 됨 → User-level은?
 

⚙️ Non-preemptive, 뺏지 않는 스케줄링

  • 평화롭게 해결, 스스로 CPU를 내놓음
    • → 절대 CPU를 내놓지 않으면 문제가 생김
notion image

⚙️ Preemptive, 뺏는 스케줄링

  • 런타임의 스케줄러가 Time interrupt 요청 → 시그널로 받게 됨
    • 소프트웨어 interrupt지만 HW→SW가 아닌 OS→SW임
notion image
 

댓글

guest