Multithreading
Multithreaded Architecture
- 대부분의 현대 애플리케이션은 멀티스레드임
- 스레드는 애플리케이션 안에서 실행
- 애플리케이션의 여러 작업은 별도의 스레드에서 실행 가능
- 화면 업데이트 / 데이터 가져오기(Fetch) / 스펠(Spell) 체크 / 네트워크 요청 응답 등
- 프로세스 생성은 무거운 작업(heavy-weight)이지만 스레드 생성은 가벼운 작업(light-weight)
- 스레드는 일부의 레지스터와 스택만 따로 가지고 코드와 데이터는 공유함
- 코드를 간단하게 할 수 있고 효율성을 높일 수 있음
- 커널은 일반적으로 멀티스레드로 동작
Multithreading의 장점
- Responsiveness 응답성 : 프로세스의 일부가 차단(block)된 상태일 때도 실행을 계속하는 것을 허용 / 특히, 사용자 인터페이스에 중요
- Resource Sharing 자원 공유 : 스레드는 프로세스의 자원을 공유 / Shared Memory 혹은 메시지 패싱보다 쉬움
- Economy 경제성 : 스레드 생성은 프로세스 생성보다 저렴 / 스레드 스위칭 오버헤드가 Context Switching 오버헤드보다 적음
- Scalability 확장성 : 프로세스는 멀티프로세서 아키텍처의 장점을 활용할 수 있음
Multicore Programming
Multicore 혹은 Multiprocessor 시스템은 프로그래머에게 다음의 도전과제에 대한 압박을 줌
- Task dividing activities 작업 분할 활동
- Load balancing 부하 분산
- Data splitting 데이터 분할
- Minimizing data dependency 데이터 종속 최소화
- Testing and debugging 테스트 및 디버깅
Parallelism 병렬성 : 시스템이 여러 작업을 동시에 수행할 수 있음
- Data parallelism : 같은 데이터 집합을 여러 코어에 분산, 각각 동일한 작업을 실행
- Task parallelism : 스레드를 코어에 분산, 각 스레드가 고유한 작업을 실행
Concurrency 병행성 : 여러 작업을 지원 / 단일 프로세서, 코어의 경우 스케줄러가 동시성을 제공
Amdahl’s Law
시리얼과 병렬 구성을 모두 가지는 애플리케이션에 추가적인 코어를 추가하면서 얻을 수 있는 성능을 식별
S = Serial Portion (시리얼 부분)
N = Processing Core 수
만약 앱이 75% 병렬 / 25% 시리얼이면 코어가 1개에서 2개가 되면 Speedup은 1.6배가 됨
N이 무한대에 가까워지면 속도 향상은 1/S로 수렴
앱의 시리얼 부분은 추가적인 코어를 추가했을 때 얻는 성능에 불균형한 영향을 미침
User Threads / Kernel Threads
User Threads : 사용자 수준 스레드 라이브러리에서 관리
3가지 주요 라이브러리 : POSIX Pthreads / Windows threads / Java threads
Kernel Threads : 커널에 의해 지원
사실상 모든 범용 OS : Windows / Solaris / Linux / Mac OS X
사용자 스레드와 커널 스레드 사이의 멀티스레딩 모델
- Many-to-One Model - 여러 사용자 수준 스레드가 하나의 커널 스레드에 매핑
- 하나의 스레드가 Block되면 모든 스레드가 Block됨
- 멀티코어 시스템에서 멀티스레드가 병렬로 작동하지 않을 수 있음
한번에 커널에 1개만 존재할 수 있기 때문
- 이 모델은 현재 거의 사용되지 않음
- 예시 : Solaris Green Thread Library / Java 초기 버전 스레드 - One-to-One Model - 각 사용자 수준 스레드는 커널 스레드와 1:1로 매핑
- 사용자 수준 스레드 생성은 커널 스레드를 생성함
- Many-to-One보다 더 많은 병행성(Concurrency)를 제공
- 프로세스당 스레드의 수가 오버헤드로 인해 제한될 수 있음
- 예시 : Windows / Linux / Solaris 9 이후 (최근 가장 많이 사용) - Many-to-Many Model - 정확히는 여러 사용자 수준 스레드가 그보다 적은 수의 커널 스레드와 매핑
- OS가 충분한 수의 커널 스레드를 생성하도록 함
- 애플리케이션은 원하는 수만큼의 사용자 스레드를 생성하고
이들에 대응하는 커널 스레드들이 다중 처리기에서 병렬로 수행될 수 있음
- Many-to-One과 One-to-One의 장점을 취한 접근법 - Two-level Model - Many-to-Many와 유사하지만 하나의 사용자 스레드가 하나의 커널 스레드에 바인딩하는 것을 허용
- Many-to-Many와 One-to-One의 혼합
- 일반적으로 사용자 스레드와 커널 스레드 사이에는 중간 데이터 구조를 사용 = Lightweight Process(LWP)
LWP는 프로세스가 사용자 스레드를 실행하도록 스케줄할 수 있는 가상 프로세서처럼 보임
각 LWP는 커널 스레드에 연결되어 있음
Explicit Threading
Thread Library : Pthreads
- 스레드 생성과 동기화를 위한 POSIX 표준 API
- Specification(명세), not Implementation(구현)
- API는 스레드 라이브러리의 동작을 지정
- 구현은 라이브러리의 개발(development)에 달려 있음
- UNIX OS에 흔히 사용됨 (Solaris, Linux, Mac OS X)
Windows Threads
Java Threads
- 자바 스레드는 JVM에 의해 관리
- 스레드 클래스를 확장하는 방법
- Runnable 인터페이스를 구현하는 방법
Implicit Threading
스레드 수 증가의 인기가 높아지고, 명시적인 스레드로 프로그램의 정확성을 유지하는 것이 어려워짐
스레드의 생성과 관리는 프로그래머보단 컴파일러와 run-time 라이브러리에 의해 수행
- Thread Pools
- 작업을 대기하면서 Pool에 여러 스레드를 생성
- 장점
1. 새로운 스레드를 생성하는 것보다 존재하는 스레드로 요청을 처리하는 것이 약간 더 빠름
2. 애플리케이션의 스레드 수를 Pool의 크기에 바인딩할 수 있게 허락
3. 생성하는 작업에서 수행하는 작업을 분리함으로써 작업 실행을 위한 다양한 전략을 허용
(작업은 주기적으로 실행되도록 스케줄될 수 있음) - OpenMP
- C 및 C++을 위한 컴파일러 지시문(directives)과 API의 집합
- Shared-memory 환경에서 병렬 프로그래밍을 지원
- 병렬 영역(parallel regions)을 식별 : 병렬로 실행할 수 있는 코드의 블록
Thread Related Issues
fork( ), exec( ) 시스템 콜
멀티스레드 프로그램에서 프로세스를 복제/생성하는 fork( ) 시스템 콜을 실행하면 새 프로세스는 모든 스레드를 복제? 혹은 메인 스레드를 가지는 하나만 복제? -> 몇몇 UNIX 시스템에서는 2가지 버전을 모두 지원
exec( ) 시스템 콜은 기존 스레드를 모두 무시하고 새로운 프로그램으로 대체
- fork( ) 이후에 exec( )가 호출되면 모든 스레드를 복제할 이유가 없음
- fork( ) 이후에 exec( )가 호출되지 않는다면, 모든 스레드의 복제가 의미 있음
Signal Handling 시그널 처리
- 시그널 : 프로세스에게 어떤 사건이 일어났음을 알리기 위해 사용
- Synchronous Signal : 어떤 사건을 처리하기 위해 발생 (예 : 불법적 메모리 접근 / 0으로 나누기)
- Asynchronous Signal : 임의 시점에 발생 (예 : 인터럽트(Ctrl-C), 타이머 만료)
- 동기식, 비동기식 둘 다 다음과 같이 전달
- 시그널은 특정 사건이 일어나야 생성
- 시그널은 생성되면 프로세스에게 전달
- 시그널이 전달되면 반드시 처리(Handling)되어야 함 - 모든 시그널은 처리기(Handler)에 의해 처리
- 디폴트 시그널 처리기 : 시그널마다 기본적인 처리 방법을 정의
- 사용자 정의 시그널 처리기 : 사용자가 시그널 처리 방법을 정의 - 단일 스레드 프로그램에서 시그널은 프로세스 레벨에서 처리됨
- 시그널을 받으면, 프로세스 수행을 중단 후, 시그널 처리기에서 해당 시그널을 처리한 후 다시 프로세스 수행 재개 - 멀티스레드 프로그램에서 시그널 처리는 다소 복잡
- 가능한 선택
1. 시그널이 적용될 스레드에게 전달 (특정 스레드가 시그널 처리기를 등록한 경우) - 모든 스레드에게 전달
2. 몇몇 스레드에게만 선택적으로 전달
3. 특정 스레드가 모든 신호를 전달받도록 지정
Thread Cancellation
- 스레드가 완료되기 전에 종료되는 것
예 : 여러 스레드가 DB를 검색하다, 한 스레드가 원하는 결과를 찾았다면 나머지 스레드는 취소
예 : 웹브라우저가 페이지를 적재하는 도중, 사용자가 Stop 버튼을 눌렀다면 관련 스레드들은 취소 - 취소될 스레드는 target thread라고 함
- Asynchronous cancellation : 타겟 스레드를 즉시 종료함
- Deferred cancellation : 타겟 스레드가 주기적으로 자신이 취소되어야 하는지 확인할 수 있도록 함
- Pthread 코드를 이용하여 스레드를 생성하고 취소함
- 스레드 취소 요청이 발생해도, 실제 취소는 스레드의 상태에 따라 결정
만약 스레드 취소가 비활성화된 경우, 취소는 활성화될 때까지 대기(pending) 상태로 유지됨
Default Type은 Deferred임 - 취소는 스레드가 취소 포인트(cancellation point)에 도달했을 때만 발생
예 : pthread_testcancel( ) - 그리고 cleanup handler가 발생 - 리눅스 시스템에서는 스레드 취소는 시그널을 통해 처리됨
Thread-Local Storage (TLS)
- Thread-Local Storage는 각 스레드가 자신의 데이터 복사본을 가질 수 있게 하는 것
- 스레드 생성 프로세스를 제어하지 않을 때 유용 (예 : 스레드 Pool을 이용할 때)
- 로컬 변수와의 차이점
- 로컬 변수는 단일 함수 호출 동안에만 볼 수 있음
- TLS는 함수 호출 간에도 보임 - Static 데이터와 유사
- TLS는 각 스레드에 고유함
'Operating System [OS]' 카테고리의 다른 글
[OS] 08. Deadlocks (1) | 2024.01.02 |
---|---|
[OS] 07. Synchronization Tools (1) | 2024.01.02 |
[OS] 05. Virtual Memory (2) | 2024.01.02 |
[OS] 04. Main Memory (2) | 2024.01.02 |
[OS] 03. CPU Scheduling (3) | 2024.01.01 |