대부분의 현대 애플리케이션은 멀티스레드 방식으로 동작한다.
커널은 일반적으로 멀티스레드 방식으로 구현된다.
서버의 경우, 클라이언트로부터 요청이 들어오면 새로운 스레드를 생성해서 해당 요청을 처리하고 서버는 추가적인 요청을 위해 계속 리스닝한다.
스레드끼리는 code, data, files는 공유하고 registers, stack, PC는 독립돼있다
프로세스 생성에 비해 스레드 생성은 가볍다.
코드를 단순화하고 효율성을 높일 수 있다.
멀티스레드 방식의 장점은 다음과 같다.
- 응답성 : 프로세스의 일부가 블로킹되어도 계속해서 실행을 지속할 수 있다.
- 자원공유 : 스레드는 프로세스의 자원을 공유하기에 공유 메모리 / 메시지 전달보다 쉽게 자원을 공유할 수 있다.
- 경제성 : 프로세스 생성보다 스레드 생성 비용이 더 적게 들고 컨텍스트 스위칭보다 스레드 스위칭이 오버헤드가 더 적다.
- 확장성 : 멀티코어 아키텍처를 활용할 수 있기에 프로세스가 확장성을 갖는다.
Multicore Programming
멀티코어 프로그래밍을 위해선 개발자가 신경써야 할 부분이 많다.
- 작업분할
- 균형
- 데이터분할
- 데이터 종속성
- 테스트 및 디버깅
병렬성(Parallelism) : 시스템이 여러 작업을 동시에 수행할 수 있다는 것
병행성(Concurrency) : 시스템이 더 많은 작업을 동시에 진행할 수 있도록 제공, 단일 프로세서/코어에서는 스케줄러가 동시성을 제공한다.
병렬성은 멀티코어 프로세서에서 여러 개의 작업이 실제로 동시에 실행되는 것이다.
병행성은 스케줄러를 통해 여러 개의 스레드를 번갈아가며 실행되는 것을 뜻한다.
데이터 병렬성(data parallelism)은 여러 개의 코어에서 같은 데이터를 병렬로 처리하는 것이다.
작업 병렬성(task parallelism)은 여러 개의 코어에서 각각의 스레드가 서로 다른 작업을 수행하는 것이다.
데이터 병렬성은 동일한 작업을 다수의 코어에서 병렬로 처리해서 처리 시간을 단축시키는데 효과적이고, 작업 병렬성은 각 스레드가 고유한 작업을 수행해서 전체 작업을 빠르게 처리하는데 효과적이다.
예를 들어, 각 코어가 데이터를 나누어 덧셈을 하는 경우를 data parallelism (소수 개수 세기)
같은 데이터에 대해 각 코어가 다른 통계 연산을 하는 경우가 task parallelism 이다.
Amdahl's Law
암달의 법칙로, 병렬 처리를 위한 알고리즘의 성능을 예측할 수 있다.
S는 0<=s<=1를 갖는 직렬화 비율이고, N은 코어의 개수이다.
예를 들어, 직렬화 비율이 25%이라면 코어를 2개 썼을 때 성능 향상은 1.6배가 된다.
N이 무한으로 간다고 했을 때, 즉 코어를 무한 개를 달았을 때 성능 향상은 1/S가 된다.
따라서 병렬 처리를 수행하는데 있어서 직렬화 비율을 최소화해야한다.
User Threads and Kernel Threads
스레드는 User-level(사용자 수준) 스레드와 kernel-level(커널 수준) 스레드로 구분할 수 있다.
사용자 수준 스레드는 스레드 관리를 스레드 라이브러리를 통해 수행하고 커널 수준 스레드는 운영체제 커널에서 직접 수행한다.
사용자 수준 스레드를 지원하는 스레드 라이브러리는 POSIX Pthreads, Windows threads, Java threads등이 있다.
커널 수준 스레드는 대부분의 일반적인 운영체제에서 지원한다.
사용자 스레드는 경량화되어 빠른 스레드 생성 및 스레드 전환을 지원한다.
커널 수준 스레드는 안정적이고 더 많은 기능과 보안을 제공한다.
Multithreading Models :: Many-to-One
Many-to-One 모델은 여러개의 사용자 수준 스레드(하나의 프로세스)가 하나의 커널 수준 스레드와 대응한다.
이는 커널 수준 스레드를 생성 및 전환하는 비용을 줄이고 사용자 수준 스레드를 빠르게 생성하고 전환한다.
하지만 Many-to-One 모델에서는 하나의 사용자 수준 스레드가 블록 상태가 되면 해당 프로세스의 모든 스레드가 블록된다.
또, 멀티코어 시스템에서는 여러개의 사용자 수준 스레드가 병렬로 실행되지 않을 수 있다.
커널 수준 스레드가 한 번에 하나씩만 실행될 수 있기 때문이다.
Multithreading Models :: One-to-One
.One-to-One 모델은 각 사용자 수준 스레드가 커널 수준 스레드와 1대1 대응한다.
따라서 사용자 수준 스레드를 생성할 때마다 커널에서 새로운 스레드를 생성하고, 이 스레드가 작업을 수행한다.
Many-to-One 모델보다 더 많은 병행성(concurrency)를 제공하고 사용자 수준 스레드 간에 블록되지 않는다.
하지만 사용자 수준 스레드를 생성할 때마다 커널에서 스레드를 생성해야하므로 오버헤드가 발생할 수 있다.
그래서 일부 운영체제에서는 프로세스당 생성 가능한 스레드 수를 제한하기도 한다.
ex) Windows, Linux
Multithreading Models :: Many-to-One
Many-to-Many 모델은 여러 개의 사용자 수준 스레드가 여러 개의 커널 수준 스레드와 대응한다.
하나의 사용자 수준 스레드가 여러 개의 커널 수준 스레드와 매핑될 수 있는 것이다.
이렇게 되면 운영체제는 적절한 수의 커널 스레드를 생성해서 사용자 스레드를 지원할 수 있다.
하지만 구현하기 어려워서 자주 사용되지는 않는다.
Multithreading Models :: Two-level
Two-level 모델은 Many-to-Many 모델과 One-to-One 모델의 장점을 합친 것이다.
중요한 작업은 One-to-One으로, 나머지는 Many-to-Many로 처리함으로써 중요한 작업에서의 기다림을 줄일 수 있다.
Thread Libraries
스레드 라이브러리는 스레드를 생성하고 관리하기 위한 API를 제공한다.
두가지 방법으로 구현할 수 있다.
1) 사용자 공간에서 완전히 라이브러리를 구현
2) OS에서 지원하는 커널 수준 라이브러리를 사용 - 커널에 시스템 콜을 발생시킴
비동기(async) 스레딩은 부모 스레드와 자식 스레드가 서로 독립적으로 병행하여 실행된다.
동기(sync) 스레딩은 부모 스레드가 모든 자식 스레드가 종료될 때까지 기다린다.
POSIX Pthreads는 사용자 수준 또는 커널 수준에서 실행될 수 있고, Windows는 커널 수준에서 실행된다.
java 스레드는 운영체제에 따라 다르다.
Pthreads
스레드 생성 및 동기화를 위한 POSIX 표준 API이며, 사용자 레벨 또는 커널 레벨로 제공될 수 있다.
명세서이고 구현된 것이 아니므로 라이브러리간 스레드의 호환성을 주의해야한다.
API는 스레드 라이브러리의 동작을 지정하고, 구현은 라이브러리 개발에 달려있다.
UNIX 운영체제에서 일반적으로 사용된다.
Implicit Threading
스레드 수가 증가함에 따라 명시적인 스레드로 프로그램의 정확성을 유지하는 것이 어려워진다.
스레드의 생성 및 관리는 개발자가 아닌 컴파일러와 런타임 라이브러리에서 수행된다.
개발자는 병렬로 수행할 수 있는 task를 식별하는데 주력하고, 스레드는 컴파일러와 런타임 라이브러리가 담당하는 것이다.
Implicit Threading :: Thread Pools
일정 수의 스레드 풀을 만들어서 작업을 대기시킨다.
장점은 다음과 같다.
- 새 스레드를 만드는 것보다 기존 스레드로 요청을 처리하는 것이 약간 더 빠르다.
- 애플리케이션에서 사용되는 스레드의 수를 풀의 크기에 바인딩할 수 있다.
- Task 생성과 실행을 분리함으로써 작업을 수행하는 다른 전략들이 가능해진다. - 작업은 주기적으로 실행되도록 예약 가능
Windows API에서 thread pools를 지원한다.
Implicit Threading :: Fork-Join Parallelism
큰 작업을 작은 작업으로 분할하고, 각각의 작은 작업을 동시에 실행하고, 결과를 모아서 최종 결과를 생성한다.
여러 개의 작은 작업으로 분할하는 과정을 fork라 하고, 병합하는 과정을 join이라고 한다.
Implicit Threading :: OpenMP
C, C++, FORTRAN을 위한 컴파일러 지시어 및 API 집합이다.
공유 메모리 환경에서 병렬 프로그래밍을 지원하고 병렬 영역을 식별해서 병렬로 실행할 수 있는 코드 블록을 나타낸다.
#pragma omp parallel 은 코어의 개수만큼 fork하고 join한다.
#pragma omp parallel for는 코어의 수만큼 스레드를 생성하고 N개의 iteration을 스레드에 분할 매핑한다.
위 예제에서는 data의 종속성이 없어서 병렬 실행이 가능하지만, 종속성이 있는 경우에는 주의해야한다.
num_threads(N) : 스레드의 개수를 N개로
schedule(dynamic) : 매핑 순서에 변화를 줌. 기존 정적인 static 방식에서 dynamic 방식으로 바꿈. 부하분산에 효과적
Implicit Threading :: Grand Central Dispatch
macOS와 iOS 운영 체제를 위한 Apple 기술
C, C++, Objective-C, API 및 런타임 라이브러리의 확장 기능
병렬 섹션을 식별할 수 있고, 스레딩의 대부분의 세부사항을 관리한다.
^{ // 이 부분을 스레드에 할당해서 실행한다. }
블록은 디스패치 큐에 배치되고, 사용 가능한 스레드가 있으면 큐에서 pop되어 할당된다.
두 가지 유형의 디스패치 큐가 있다.
직렬(serial) : 블록이 한 번에 하나씩 FIFO로 제거된다. 큐는 프로세스당 하나의 큐로, 메인 큐라고도 한다.
개발자는 프로그램 내에서 추가적인 시리얼 큐를 만들 수 있다.
병행(concurrent) : FIFO 순서로 제거되지만 한 번에 여러 블록을 제거할 수도 있다.
* QoS(서비스 품질)에 따라 4개의 시스템 전역 큐가 나뉜다.
Implicit Threading :: Intel Threading Building Blocks (TBB)
병렬 C++ 프로그램을 디자인하기 위한 템플릿 라이브러리다.
위와 같이 간단한 for루프의 직렬 버전이 있을 때, TBB를 사용한 동일한 for 루프는 다음과 같다.
Semantics of fork() and exec()
fork()를 호출할 때, 보통 호출한 스레드만 복제하지만 일부 UNIX 운영체제에서는 모든 스레드를 복제할 수도 있다.
exec() 시스템콜은 현재 실행중인 프로세스를 새로운 프로세스로 대체하는데 이 경우엔 호출한 스레드만 복제하는 것이 유리하다.
리눅스는 호출한 스레드만 복제한다.
스레드와 fork()는 함께 사용하지 않는 것이 좋다.
Signal Handling
UNIX 시스템에서 시그널은 특정 이벤트가 발생했음을 프로세스에 알리는데 사용된다. 시그널 핸들러는 시그널을 처리하는데 사용된다.
1. 특정 이벤트에 의해 시그널이 생성됨.
2. 시그널이 프로세스에 전달됨.
3. 두 가지 시그널 핸들러 중 하나가 시그널을 처리함.
3-1. 기본 시그널 핸들러
3-2. 사용자 정의 시그널 핸들러
모든 시그널은 커널이 시그널을 처리할 때 실행하는 기본 핸들러를 가지고 있다.
사용자 정의 시그널 핸들러는 기본 핸들러를 대체할 수 있다.
단일 스레드에서는 시그널이 프로세스에 전달된다.
멀티스레드 프로그램에서는 여러 스레드가 동시에 실행되므로 시그널을 어떤 스레드로 전달할지 결정하는 것이 중요하다.
다음은 시그널을 멀티스레드 프로그램에서 전달하는 방법에 대한 몇 가지 옵션이다.
- 시그널이 적용되는 스레드에 전달 (ex- divide by zero)
- 프로세스 내 모든 스레드에게 시그널 전달 (ex- Ctrl-C)
- 프로세스 내 특정 스레드에게 시그널 전달
- 특정 스레드를 시그널 핸들러로 할당
대부분의 Unix 계열 OS에서는 스레드마다 받는, 또는 거부하는 signal을 설정할 수 있다.
따라서 멀티스레드인 경우 signal을 허용하는 첫 번째 스레드가 처리한다.
Thread Cancellation
스레드가 완료되기 전에 종료해야 하는 경우, target thread를 종료해야 한다.
일반적인 두 가지 접근 방식은 다음과 같다.
- Async cancellation : 취소 대상 스레드를 즉시 종료한다.
- Deferred cancellation : 취소 대상 스레드가 주기적으로 취소 여부를 확인할 수 있도록 한다.
- * 취소 예약 플래그를 설정해두면 다음 취소 가능 지점(cancellation point)에서 스레드를 취소한다.
스레드 취소 요청을 호출하면 취소 예약이 되지만, 실제 취소는 스레드 상태에 따라 다르다.
스레드 취소 비활성화(cacellation disabled) 상태인 경우 취소 요청은 보류 상태로 남아있는다.
취소 요청을 처리하려면 스레드가 취소 가능(cacellation enabled) 상태가 되어야 한다.
취소 가능 상태에서는 기본적으로 지연 취소 방식이 사용된다. 이 경우 취소 지점(cancellation point)에 도달할 때까지 스레드가 실행되고
취소 지점에 도달하면 취소가 발생하고 cleanup handler가 호출된다.
Linux 시스템에서는 스레드 취소가 signal을 통해 처리된다. 스레드 취소 signal이 발생하면 취소 예약 플래그가 설정되고,
취소 지점에 도달할 때까지 스레드가 계속 실행된다. 따라서 취소 가능 지점을 잘 설정해야 한다.
취소 가능 지점을 설정하는 함수는 다음과 같다 - pthread_testcancel()
Thread Local Storage (TLS)
스레드 지역 저장소는 각 스레드가 자신만의 데이터 복사본을 가질 수 있게 해주는 기능이다.
TLS는 스레드 생성 프로세스를 직접 제어할 수 없을 때 유용하다.
예를 들어, 스레드 풀을 사용하는 경우, 어떤 스레드가 언제 생성될지 알 수 없으므로 TLS를 사용해서 각 스레드가 자신의 데이터를 안전하게 유지할 수 있다.
TLS는 로컬 변수와는 다르게, 함수 호출 반환 간에 유지되며 각 스레드마다 별도의 데이터를 가진다.
따라서 정적 데이터(전역 변수)와 유사하지만 각 스레드마다 고유한 복사본을 가지기에 다른 스레드가 접근할 수 없다.
Linux Threads
리눅스에서는 스레드 대신 task라고 부른다.
리눅스에서 스레드 생성은 clone() 시스템 호출을 통해 이뤄지고, clone() 함수는 자식 task가 부모 task와 동일한 주소 공간을 공유할 수 있도록 해준다. 이때 플래그를 사용해서 동작을 제어할 수 있다.
스레드나 task 생성 시 생성된 스레드나 task를 관리하기 위해서 커널은 각각의 task에 대한 데이터를 갖고 있는다.
리눅스에서는 struct task_struct라는 구조체를 이용해 이런 데이터를 저장한다.
이 구조체는 프로세스 데이터 구조체를 가리키며, 공유할 수도 있고 고유한 구조체를 사용할 수도 있다.
fork()는 task_struct를 copy하고 clone()은 point - 링크로 처리한다. (참조한다)
'학교강의필기장 > 운영체제론' 카테고리의 다른 글
운영체제론[9]: CPU 스케줄러 2 (0) | 2023.04.24 |
---|---|
운영체제론[8]: CPU 스케줄러 1 (0) | 2023.04.24 |
운영체제론[6]: 프로세스 간 통신, RPC (0) | 2023.04.24 |
운영체제론[5]: 프로세스, 공유메모리와 매시지패싱 (0) | 2023.04.24 |
운영체제론[4]: 시스템 서비스, 운영체제 구조 - 커널, 시스템 부트 및 디버깅 (0) | 2023.04.24 |