공룡책(운영체제)을 읽고 정리한 글입니다.
스레드가 필요한 이유
예를 들어, 웹 서버는 클라이언트로부터 웹 페이지나 이미지, 소리 등에 대한 요청을 받는다.
하나의 웹 서버는 여러 개의 클라이언트들의 병행하게 접근할 수 있다. 만약 웹 서버가 단일 스레드 프로세스로 작동한다면, 자신의 단일 프로세스로 한 번에 하나의 클라이언트만 서비스할 수 있게 되어 클라이언트는 자신이 요청이 서비스되기까지 매우 긴 시간을 기다려야 할 것이다.
하지만 멀티 스레드 환경에서는 아래와 같은 구조로 요청-응답구조가 바뀐다.
요청이 들어오면 새로운 프로세스를 생성하지 않고, 요청을 서비스 할 스레드를 생성하고 추가적인 요청을 받기위한 작업을 재개한다.
멀티 스레드 프로세스의 장점
- 응답성
실시간 애플리케이션을 멀티 스레드화 하면 애플리케이션의 일부분이 동기식 작업에 의해 멈추거나, 긴 작업을 수행하더라도 프로그램의 수행이 계속되는 것을 허용함으로써, 사용자에 대한 응답성을 증가시킨다. - 자원 공유
프로세스는 공유 메모리와 메시지 전달 기법을 통해서만 자원을 공유할 수 있다.
이러한 기법은 명시적으로 개발자에 의해 처리되어야 한다.
그러나 스레드는 자동으로 그들이 속한 프로세스의 자원들과 메모리를 공유한다. - 경제성
프로세스 생성에는 메모리와 자원의 할당이 필요하기 때문에 비용이 많이 든다.
하지만 스레드는 자신이 속한 프로세스의 자원을 공유하기 때문에 스레드에서의 context switch가 훨씬 경제적이다.
오버헤드의 차이를 경험적으로 측정하기는 어려울 수 있지만, 일반적으로 스레드 생성은 프로세스 생성보다 메모리를 덜 소비한다. - 규모 적응성(확장성)
멀티 스레드의 이점은 멀티 코어 구조에서 더욱 증가한다.
멀티 코어 구조에서는 각각의 스레드가 다른 프로세서에서 병렬로 수행될 수 있기 때문이다.
단일 스레드 프로세스는 코어가 아무리 많아도 오직 하나의 코어에서만 실행된다.
멀티 코어 프로그래밍
우선 싱글 코어 4스레드 환경에서 프로그램을 수행하는 상황을 살펴보자
싱글 코어가 있는 시스템에서는 처리 코어가 한 번에 하나의 스레드만 실행할 수 있다.
따라서 싱글 코어 시스템에서의 병행성은 시간이 지남에 따라서 스레드 실행이 됨을 의미한다.
그러나 멀티 코어 시스템에서의 병행성은 일부 스레드가 병렬로 실행될 수 있음을 의미한다.
병행 시스템 : 모든 작업이 진행되게 하여 둘 이상의 작업을 지원한다.
병렬 시스템 : 둘 이상의 작업을 동시에 진행하게 한다.
따라서 병렬성 없이 병행성을 가질 수 있게 된다.
과거 싱글 코어 시스템에서는 CPU 스케줄러가 프로세스간의 빠른 context switching을 통해서 각 프로세스가 병렬로 진행된다는 환상을 제공하는 방법을 사용했다.
멀티 코어 프로그래밍을 위한 과제
- 태스크 인식
애플리케이션을 분석하여 독립된 병행 가능 태스크로 나눌 수 있는 영역을 찾는 작업이 필요하다.
태스크는 서로 독립적이므로, 개별 코어에서 병렬 실행될 수 있어야 한다. - 균형
병렬로 실행될 수 있는 태스크를 찾아내는 것도 중요하지만 찾아진 부분들이 전체 작업에 균등한 기여도를 가지도록 태스크로 나누는 것도 매우 중요하다.
임의의 태스크를 나누고, 나눠진 태스크 일부의 기여도가 매우 적어진다면 별도의 코어를 사용하는 것은 그만한 가치가 없다. - 데이터 분리
태스크가 나눠지면 태스크에서 접근하고 조작하는 데이터 또한 개별 코어에서 사용할 수 있도록 나누어져야 한다. - 데이터 종속성
태스크가 접근하는 데이터는 둘 이상의 태스크 사이에 종속성이 없는지 검토되어야 한다.
(자원 동시 접속 문제) - 시험 및 디버깅
프로그램이 멀티 코어에서 실행될 때, 다양한 실행 경로가 존재할 수 있다.
따라서 프로그램의 시험 및 디버깅이 매우 어려워진다.
암달의 법칙(Amdahl's Law)
N개의 처리 코어를 가진 시스템에서 실행되는 애플리케이션 중에서 실제로 순차적으로 실행되어야만 하는 구성요소의 %를 S라고 하면 공식은 다음과 같이 표현된다.
예를 들어 75%의 병렬 실행 구성요소와 25% 순차실행 구성요소를 구진 애플리케이션이 있다.
이 애플리케이션을 2코어 시스템에서 실행시킬 경우, 약 1.6배의 속도향상을 얻을 수 있다.
병렬 실행의 유형
데이터 병렬 실행
동일한 데이터의 부분집합을 다수의 계산 코어에 분배한 뒤 각 코어에서 동일한 연산을 실행하는 데 초점을 맞춘다.
ex) arr = [1, 2, ... 9, 10] 이 주어졌을 때 sum(arr)을 구하면?
- 싱글 코어에서는 하나의 스레드에서 arr[0]부터 arr[9]까지 모두 더한다.
- 멀티 코어에서는 A 스레드에서 arr[0]부터 arr[4]까지 더하고, B스레드에서 arr[5]부터 arr[9]까지 더한 후 둘의 값을 더한다.
태스크 병렬 실행
데이터가 아니라 태스크(스레드)를 다수의 코어에 분배한다.
기본적으로 데이터 병렬 처리에는 여러 코어에 데이터를 분배하는 것이 포함된다.
태스크 병렬 처리에는 위 그림에 표시된 것처럼 여러 코어에 태스크를 분배하는 것이 포함된다.
데이터와 태스크 병렬 처리는 상호 배타적이지 않으며 실제로 애플리케이션에서 두 가지 전략을 혼합해서 사용할 수 있다.
멀티 스레드 모델
사용자 스레드는 커널 위에서 지원되고, 커널의 지원없이 관리된다. (커널을 통해 생성되지만 커널이 이후 관리를 하지 않는다는 뜻인가)
반면에 커널 스레드는 운영체제에 의해 직접 지원되고 관리된다.
1. 다대일 모델
많은 사용자 스레드를 하나의 커널 스레드로 매핑한다.
스레드는 사용자 공간의 스레드 라이브러리를 통해서 관리한다.
하지만, 한 스레드가 동기식 시스템 콜을 할 경우, 전체 프로세스가 봉쇄된다.
또한, 한 번에 하나의 스레드만이 커널에 접근할 수 있기 때문에, 다중 스레드가 다중 코어 시스템에서 병렬로 실행될 수 없다.
대표적인 예로 그린 스레드(Green thread)가 다대일 모델을 사용했는데, 그린 스레드는 Solaris 시스템을 위한 스레드 라이브러리를 의미하고, 초기 Java에서도 사용되었다.
이후 멀티 코어 시스템이 표준이 되면서 이 모델은 사라진다.
2. 일대일 모델
각 사용자 스레드를 각각 하나의 커널 스레드로 매핑하는 방법이다.
이 모델은 하나의 스레드가 동기식 시스템 콜을 호출하더라도 다른 스레드에 영향을 주지 않는다.
따라서 멀티 코어 시스템의 병렬 처리를 지원한다.
하지만 많은 수의 사용자 스레드를 만들기 위해서는 그만큼 커널 스레드를 만들어야 하기 때문에 시스템 성능에 영향을 줄 수 있다.
3. 다대다 모델
여러 개의 사용자 스레드를 여러 개의 커널 스레드로 매핑한다. (사용자 스레드와 커널 스레드의 개수는 다를 수 있음)
커널 스레드의 수는 애플리케이션이나 특정 하드웨어에 의해 결정된다.
다대다 모델은 앞서 보았던 다대일, 일대일 모델의 단점을 보완했다.
어느 정도의 병렬 실행을 획득하고, 필요한 만큼만의 커널 스레드를 만들어 시스템 성능을 높일 수 있다.
여기서 사용자와 커널 스레드의 일대일 매핑을 허용하는 방식을 두 수준 모델이라고 부른다.
다대다 모델은 실제로 구현하기가 어렵다.
따라서 현대 운영체제는 위와 같은 두 수준 모델을 통해서 다대다 모델이지만 ,일대일 모델도 함께 사용할 수 있는 모델을 사용한다.
스레드 라이브러리
암묵적 스레딩
모든 스레드의 동작을 관리하는 프로그램을 만드는 것은 어려운 일이다. 이러한 어려움을 극복하고 병행 및 병렬 프로그램의 설게를 도와주는 한 가지 방법은 스레딩의 생성과 관리 책임을 개발자로부터 컴파일러와 런타임 라이브러리에게 넘겨주는 것이다. 이를 암묵적 스레딩이라고 부르고 이 전략은 점점 널리 사용되고 있다.
암묵적 스레딩을 활용한 프로그램을 설계하는 4가지 방법을 알아보자
1. 스레드 풀
다중 스레드로 구성된 웹브라우저를 생각해보자.
웹 서버는 요청을 받을 때마다 그 요청을 위해 새로운 스레드를 만들어 준다. 새로운 스레드를 매 요청마다 만들어 주는 것은, 그때마다 새로운 프로세스를 만들어주는 것보다는 확실히 더 진보된 방법이지만, 다중 스레드 서버는 아직도 여러 문제를 가지고 있다.
첫 번째 문제는 서비스할 때마다 스레드를 생성하는 데 소요되는 시간이다. 특히 만들어진 스레드가 사용되는 시간이 짧을수록 비효율적일 것이다.
두 번째 문제는 동시에 실행할 수 있는 최대 스레드 수가 몇 개인지 한계를 정해야 한다.
이러한 문제들을 해결할 수 있는 방법 중 하나가 스레드 풀 방법이다.
기본 아이디어
프로세스를 시작할 때 아예 일정한 수의 스레드들을 미리 풀로 만들어두는 것이다. 이 스레드들은 평소에는 하는 일 없이 일감을 기다린다. 서버는 추가적인 스레드를 생성하지 않고 요청을 받으면 대신 스레드 풀에 제출하고 추가 요청 대기를 재개한다. 풀에 사용 가능한 스레드가 있으면 깨어나고 요청이 즉시 서비스된다. 풀에 사용 가능한 스레드가 없으면 사용 가능한 스레드가 생길 때까지 작업이 대기된다. 스레드가 서비스를 완료하면 풀로 돌아가서 더 많은 작업을 기다린다.
장점
- 새 스레드를 만들어 주기보다 기존 스레드로 서비스해 주는 것이 종종 더 빠르다.
- 스레드 풀은 임의 시각에 존재할 스레드 개수에 제한을 둔다. 이러한 제한은 많은 수의 스레드를 병렬 처리할 수 없는 시스템에 도움이 된다.
- 태스크를 생성하는 방법을 태스크로부터 분리하면 태스크를 실행을 다르게 할 수 있다.
예를 들어 태스크를 일정 시간 후에 실행되도록 스케줄 하더나 혹은 주기적으로 실행시킬 수 있다.
2. Fork Join
이 메소드를 사용하면 메인 부모 스레드가 하나 이상의 자식 스레드를 생성(fork)한 다음 자식의 종료를 기다린 후 join하고 그 시점부터 자식의 결과를 확인하고 결합할 수 있다.
운영체제 사례
Windows 스레드
Windows 앱들은 프로세스 형태로 실행되며 각 프로세스는 한 개 또는 그 이상의 스레드를 가질 수 있다.
스레드의 일반적인 구성요소
- 각 스레드를 유일하게 지목하는 스레드 ID
- 처리기의 상태를 나타내는 레지스터 집합
- 프로그램 카운터
- 사용자 모드에서 실행될 때 필요한 사용자 스택, 커널모드에서 실행될 때 필요한 커널 스택
- 실행 시간 라이브러리와 동적 링크 라이브러리등이 사용하는 개별 데이터 저장 영역
레지스터 집합, 스택, 개별 데이터 저장 영역들은 그 스레드의 문맥(Context)이라 불린다.
윈도우 스레드의 주요 자료구조
- ETHREAD - 실행 스레드 블록
스레드가 속한 프로세스를 가리키는 포인터와 그 스레드가 실행을 시작해야 할 루틴의 주소등을 가지고 있다.
그리고 KTHREAD에 대한 포인터도 가지고 있다. - KTHREAD - 커널 스레드 블록
스레드의 스케줄링 및 동기화 정보를 가지고 있다. 그리고 스레드가 커널 모드에서 실행될 때 사용되는 커널 스택과 TEB에 대한 포인터를 가지고 있다. - TEB - 스레드 환경 블록
ETHREAD와 KTHREAD는 모두 커널 안에 존재한다. 이는 커널만이 읻르을 접근할 수 있다는 것을 의미한다.
TEB는 사용자 모드에서 실행될 때 접근되는 사용자 공간 자료구조이다. TEB는 스레드 식별자, 사용자 모드 스택 및 스레드 국지 저장소(내부 로컬 저장소)를 저장하기 위한 배열을 가지고 있다.
Linux 스레드
Linux는프로세스를 복제하는 기능을 가진 fork()
시스템 콜을 제공한다. 그리고 clone() 시스템 콜을 이용하여 스레드를 생성할 수 있는 기능도 제공한다. 그러나 Linux는 프로세스와 스레드를 구별하지 않는다.
Linux는 프로그램 내의 제어 흐름을 나타내기 위하여 프로세스나 스레드보다 태스크라는 용어를 사용한다.
clone()이 호출될 때 일어나는 일
- 부모와 자식 태스크가 자료구조를 얼마나 공유할지 결정하는 플래그의 집합이 전달된다.
(위 그림에 있는 flag를 모두 전달받았다고 가정하자) - 부모 태스크와 자식 태스크는 같은 파일 시스템 정보, 같은 메모리 공간, 같은 신호처리기, 같은 열린 파일의 집합을 공유하게 된다.
- 이렇게 부모 태스크와 자식 태스크가 거의 모든 자원을 공유하기 때문에 이 챕터에서 본 스레드를 생성하는 것과 같은 결과가 된다. - 만약 아무 flag 없이 clone() 이 호출되면 정보 공유는 일어나지 않게되고, fork() 시스템 콜이 제공하는 기능과 유사간 기능을 제공한다.
Linux 스레드의 특징
fork()를 호출하면 부모 프로세스의 관련 자료구조를 복사함으로써 새로운 태스크를 생성한다.(공유 X)
clone()은 전달되는 flag에 따라 부모 태스크의 자료구조를 가리킨다. clone() 시스템 콜의 융통성은 컨테이너 개념으로 확장될 수 있다. clone()을 통해 서로 격리된 환경인 컨테이너 환경을 생성하는 것을 가능하게 한다. 이후 Linux 컨테이너 관련 부분에서 자세히 다루겠다.
요약
- 스레드는 CPU 사용의 기본 단위를 다나태며 동일한 프로세스에 속하는 스레드는 코드 및 데이터를 포함하여 많은 프로세스 자원을 공유한다.
- 다중 스레드 애플리케이션에는 1.응답성, 2.자원 공유, 3.경제성, 4.확장성 이라는 4가지 주요 이점이 있다.
- 여러 스레드가 진행 중인 경우 병행성이 존재하는 반면에, 여러 스레드가 동시에 진행 중인 경우 병렬성이 존재한다.
단일 CPU가 있는 시스템에서는 오로지 병행성만 가능하고, 병렬성은 여러 CPU를 제공하는 다중 코어 시스템이 필요하다. - 다중 스레드 애플리케이션의 도전과제.
1.작업 분할 및 균형 조정
2.서로 다른 스레드 간에 데이터 분할 및 데이터 종속성 식별
3.테스트 및 디버깅의 어려움 - 데이터 병렬 처리는 동일한 데이터의 부분 집합을 다른 컴퓨팅 코어에 분산시키고, 각 코어에서 동일한 연산을 수행한다. 작업 병렬 처리는 여러 코어에 데이터가 아니라 작업을 분산시킨다. 각 작업은 고유한 연산을 실행한다.
- 사용자 응용 프로그램은 사용자 수준 스레드를 생성하며, 이 스레드는 궁극적으로 CPU에서 실행되도록 커널 스레드에 매핑되어야 한다. 다대일 모델은 많은 사용자 수준 스레드를 하나의 커널 스레드에 매핑한다.(전체 스레드 봉쇄의 위험성)
- 암묵적 스레딩에는 스레드가 아니라 작업을 식별하고 언어 또는 API 프레임워크가 스레드를 만들고 관리할 수 있게 한다. 스레드 풀, fork join 프레임워크 및 GCD(Grand Central Dispatch)를 포함하여 암묵적 스레딩에 대한 몇 가지 접근 방식이 있다. 암묵적 스레딩은 병행 및 병렬 프로그램을 개발할 때 점점 보편적인 기술이 되고 있다.
- 스레드는 비동기 또는 지연 취소를 사용하여 종료될 수 있다. 비동기 취소는 스레드가 업데이트를 수행하는 중이라도 스레드를 중지한다. 지연 취소는 스레드에 종료를 통지하지만 스레드는 질서있게 종료된다. 주로 지연 취소를 선호한다.
- Linux는 프로세스와 스레드를 구분하지 않는 대신 각각을 태스크라고 한다. Linux clone() 시스템 콜을 사용하여 프로세스와 더 비슷하거나 스레드와 더 비슷한 태스크를 만들 수 있다.
'CS > OS' 카테고리의 다른 글
[운영체제] Ch6. 동기화 도구들 (0) | 2022.02.22 |
---|---|
[운영체제] Ch5. CPU 스케줄링 (0) | 2022.02.18 |
[운영체제] Ch3. 프로세스 (0) | 2022.02.09 |
[운영체제] Ch2. 운영체제 구조 (0) | 2022.02.06 |
[운영체제] Ch1. 서론 (0) | 2022.02.01 |