[4] 스레드와 병행 (Threads & Concurrency)

준제로·2023년 8월 26일

3장에서 소개한 프로세스 모델은 한 프로세스가 하나의 제어 스레드로 프로그램을 실행한다고 가정하였다. 그러나 거의 모든 현대 운영체제는 한 프로세스가 다중 스레드를 포함하는 특성을 제공한다.

4.1 개요

스레드는 CPU 이용의 기본 단위이다.

스레드는 다음으로 구성된다.

  • 스레드ID
  • 프로그램 카운터 PC
  • 레지스터 집합
  • 스택

스레드는 같은 프로세스에 속한 다른 스레드와 코드, 데이터 섹션, 그리고 열린 파일이나 신호와 같은 운영체제 자원들을 공유한다. 프로세스가 다수의 제어 스레드를 가진다면, 프로세스는 동시에 하나이상의 작업을 수행할 수 있다. 다음 그림은 단일 스레드 프로세스와 다중 스레드 프로세스의 차이점을 보여주고 있다.

4.1.1 동기_Motivation

프로세스 생성 작업은 매우 많은 시간을 소비하고 많은 자원을 필요로 한다. 하지만 새 프로세스가 해야할 일이 기존 프로세스가 하는 일과 동일하다면 이렇게 많은 오버헤드를 감수해야할 필요가 없다. 대부분은 그렇게 하는 것보다 프로세스 안에 여러 스레드를 만들어 나가는 것이 더 효율 적이다.

웹 서버가 다중 스레드화되면 서버는 클라이언트의 요청을 listen하는 별도의 스레드를 생성한다. 요청이 들어오면 다른 프로세스를 생성하는 게 아니라, 요청을 서비스할 새로운 스레드를 생성하고 추가적인 요청을 listen하기 위한 작업을 재개한다 다음 그림에 설명되어 있다.

4.1.2 장점_Benefits

다중 스레드 프로그래밍의 이점은 다음의 4가지 큰 부류로 나눌 수 있다.

  1. 응답성(responsiveness): 시간이 오래 걸리는 연산이 별도의 비동기적 스레드에서 실행된다면 응용은 여저히 사용자에게 응답할 수 있다.
  2. 자원 공유(resource sharing): 프로세스는 공유 메모리와 메세지 전달 기법을 통하여만 자원을 공유할 수 있다. 이러한 기법은 프로그래머에 의해 명시적으로 처리되어야한다. 그러나 스레드는 자동으로 그들이 속한 프로세스의 자원들과 메모리를 공유한다.
  3. 경제성(economy) : 프로세스 생성을 위해 메모리와 자원을 할당하는 것은 비용이 많이든다. 스레드는 자신이 속한 프로세스의 자원들을 공유하기 때문에, 스레드를 생성하고 문맥 교환하는 것이더욱더 경제적이다. 오버헤드의 차이를 경험적으로 측정하는 것은 어려울 수 있지만 일반적으로 스레드 생성은 프로세스 생성보다 시간과 메모리를 덜 소비하며, 문맥교환또한 프로세스 사이보다 스레드 사이에서 빠르다.
  4. 규모 적응성(scalability): 다중 스레드의 이점은 다중 처리기 구조에서 더욱 증가할 수 있다. 다중 처리기 구조에서는 각각의 스레드가 다른 처리기에서 병렬로 수행될 수 있기 때문이다.

4.2 다중 코어 프로그래밍_Multicore Programming

단일 CPU 시스템은 다중 CPU 시스템으로 발전했다. 나중의 비슷한 시스템 설계 추세는 단일 컴퓨팅 칩에 여러 컴퓨팅 코어를 배치하는 것이다. 각 코어는 운영체제에 별도의 CPU로 보여지게 된다. 이러한 시스템을 다중 코어라고 한다.

다중 스레드 프로그래밍은 이러한 여러 컴퓨팅 코어를 보다 효율적으로 사용하고, 병행성을 향상시키는 기법을 제공한다.

병행성과 병렬성의 차이

병행시스템(Concurrent system)은 모든 작업이 진행되게 하여 둘이상의 작업을 지원하는 것으로 단일 코어로도 가능하다. 병렬 시스템(parallel system)은 둘 이상의 작업을 동시에 실행하는 것으로, 멀티 코어가 필요하다. 따라서 병렬성없이 동시성을 가지는 것은 가능하다. 기존 단일코어 시스템서, CPU스케쥴러는 프로세스 전환을 매우 빠르게 하여, 병렬성의 환상을 느끼게 한 것이다. 이러한 프로세스는 병행하게 실행되었지만 병렬로 실행되지는 않았다.

4.2.1 프로그래밍 도전과제

운영체제 설계자는 병렬 수행이 될 수 있도록 여러 코어를 활용하는 스케줄링 알고리즘을 개발해야한다. 응용프로그래머는 기존 프로그램을 다중 스레드를 활용하도록 수정해야한다. 일반적으로 다중 코어 시스템을 위해 프로그래밍하기 위해서는 5개의 극복해야할 도전과제가 있다.

  1. 테스크 인식: 병렬로 실행될 수 있는 태스크를 찾아내야한다. 이상적으로 태스크는 서로 독립적이므로 개별 코어에서 병렬 시행될 수 있어야한다.
  2. 균형: 태스크들이 전체작업에서 균등한 기여도를 가지도록 나눠야한다. 어떤 경우, 다른 태스크에 비해 기여도가 적은 작업이 있을 수 있으며 이러한 작업을 실행하기 위해 별도의 코어를 사용하는 것은 그만한 가치가 없다.
  3. 데이터 분리: 태스크가 접근하는 데이터 또한 개별 코어에서 사용할 수 있도록 나누어져야한다.
  4. 데이터 종속성: 태스크가 접근하는 데이터는 둘이상의 테스크 사이에서 종속성이 없는 지 검토해야한다. 만약 종속적인 경우, 프로그래머가 데이터 종속성을 수용할 수 있도록 태스크의 수행을 잘 동기화 해야한다. (이 전략은 6장에서 검토)
  5. 시험 및 디버깅: 프로그램이 다중 코어에서 병렬로 실행되면, 다양한 실행 경로가 존재할 수 있다 . 이런 병행 프로그램을 시험하고 디버깅하는 것은 단일 스레드보다 근본적으로 훨씬 어렵다.

⇒ 이런 도전과제 때문에 개발자들은 다중 코어 시스템의 확산으로 인하여 향후 소프트웨어를 설계하기 위한 완전히 새로운 접근법이 필요하다고 주장한다.

4.2.2 병렬 실행의 유형

데이터 병렬 실행과 태스크 병렬 실행의 두가지 유형이 존재한다.

데이터 병렬 실행: 같은 데이터의 부분 집합들을 멀티 코어들에 분배한 뒤, 각 코어에서 동일한 연산을 수행한다.

예) 배열에서 0~N-1번 째 원소의 합 ⇒단일 코어에서는 하나의 쓰레드가 원소 0~N-1까지를 더해야한다. 듀얼 코어에서는 반으로 나눠 더한 뒤 두 스레드에서 병렬적으로 실행시킬 수 있다.

태스크 병렬 실행: 데이터가 아니라 태스크(스레드)를 다수의 코어에 분배한다. 각 스레드는 고유의 연산을 실행한다. 동일한 데이터에 대해 연산을 실행할 수도 있고, 다른 데이터에 대해 연산을 실행할 수 도 있다.

⇒ 그러나 데이터와 태스크 병렬 처리는 상호 배타적이지 않으며 실제로 응용 프로그램은 이 두가지 전략을 혼합하여 사용할 수 있다.

4.3 다중 스레드 모델_Multithreading Models

지금까지는 일반적 의미에서 스레드를 다루어 왔다. 그러나 사용자 스레드는 사용자 수준에서의 지원이, 커널 스레드는 커널 수준에서의 지원이 제공된다.

사용자 스레드: 커널 위에서 지원되며 커널의 지원 없이 관리된다.

커널 스레드: 운영체제에 의해 직접 지원되고 관리된다.

⇒ 궁극적으로 사용자 스레드와 커널 스레드는 어떤 연관관계가 존재해야한다. 이 절에서는 이 연관 관계를 확립하는 세가지 일반적인 다대일, 일대일, 다대다 모델을 살펴보겠다.

4.3.1 다대일 모델_Many-to-One Model

많은 사용자 수준 스레드를 하나의 커널 스레드로 사상한다. (유의할 필요가 있는 현상의 특징 이외의 다른 성질을 버리다.) 스레드 관리는 사용자 공간의 스레드 라이브러리에 의해 행해지므로 효율적이라 할 수 있으나, 한 스레드가 봉쇄형 시스템 콜을 할 경우, 전체 프로세스가 봉쇄된다. 그리고, 한번에 하나의 스레드만이 커널에 접근할 수 있기 때문에, 다중 스레드가 다중 코어 시스템에서 병렬로 시행될 수 없다. ⇒ 다중 처리 코어의 이점을 살릴 수 없기 때무에 이 모델을 사용중인 시스템은 거의 존재하지 않는다.

4.3.2 일대일 모델_One-to-One Model


각 사용자 스레드를 각가 하나의 커널 스레드로 사상한다. 따라서 하나의 스레드가 봉쇄적 시스템 콜을 호출하더라도 다른 스레드가 실행될 수 있기 때문에 다대일 모델보다 더 많은 병렬성을 제공한다. 또한 다중 처리기에서 다중 스레드가 병렬로 수행되는 것을 허용한다. 이 모델의 유일한 단점은 많은 수의 커널 스레드가 시스템 성능에 부담을 줄 수 있다는 것이다. Linux는 Windows 운영체제 제품군과 함께 일대일 모델을 구현한다.

4.3.3 다대다 모델_Many-to-Many Model

다대다 모델은 여러개의 사용자 수준 스레드를 그보다 작거나 같은 수의 커널 스레드로 멀티 플렉스한다. 이러한 설계가 병행 실행에 미치는 영향을 생각해보자.

다대일 모델은 개발자가 원하는 만큼의 사용자 수준 스레드를 생성하도록 허용하지만, 커널은 한번에 하나의 커널 스레드만 스케줄 할 수 있기 때문에 진정한 병렬 실행을 획득할 수 없다

일대일 모델은 더 많은 병행 실행을 제공하지만, 개발자가 한 응용내에 너무 많은 스레드를 생성하지 않도록 주의해야한다.

⇒ 다대다 모델은 이러한 두가지 단점을 어느정도 해결했다.

: 개발자는 필요한 만큼 많은 사용자 수준 스레드르 생성할 수 있다. 그리고 상응하는 커널 스레드가 다중 처리기에서 병렬로 수행될 수 있다. 또한, 스레드가 봉쇄형 시스템 콜을 발생시켰을 대, 커널이 다른 스레드의 수행을 스케줄 할 수 있다.

다대다 모델이 가장 융통성있는 모델로 보이지만 실제론 구현하기가 어렵다. 그리고 처리 코어수가 증가함에 따라 커널 스레드 수를 제한하는 것의 중요성이 줄어들었다.

⇒ 결과적으로 대부분의 운영체제는 이제 일대일 모델을 사용한다.

4.4 스레드 라이브러리

스레드 라이브러리는 프로그래머에게 스레드를 생성하고 관리하기 위한 API를 제공한다.

스레드 라이브러리를 구현하는 데에는 주된 두가지 방법이 있다.

  1. 커널의 지원 없이 완전한 사용자 공간에서만 라이브러리를 제공

    라이브러리의 함수를 호출하는 것은 시스템 콜이 아니라 사용자 공간의 지역함수를 호출하게 되는 것이다.

  2. 운영체제에 의해 지원되는 커널 수준 라이브러리르 구현하는것

    라이브러리를 위한 코드와 자료구조는 커널 공간에 존재하므로 라이브러리 API를 호출하는 것은 커널 시스템 콜을 부르는 것이 된다.

현재 POSIX Pthreads, Windows 및 Java의 세종류 라이브러리가 주로 사용된다.

  • Windows는 Windows 시스템 전용이다
  • Pthreads는 UNIX,Linux 및 macOS와 같은 POSIX 호환 시스템에서 사용가능하다.
  • Java 스레드는 자바 가상머신을 지원하는 모든 시스템에서 실행된다.

cf) 동기스레딩과 비동기 스레딩

동기 스레딩: 부모 스레드가 하나 이상의 자식 스레드를 생성하고 자식 스레드가 모두 종료할 때까지 기다렸다가 자신의 실행을 재개하는 방식이다. 통상 동기 스레딩은 스레드 사이의 상당한 데이터 공유를 수반한다. (ex) 부모 스레드는 자식들이 계산한 결과를 통합할 수 있다.)

비동기 스레딩: 부모가 자식 스레드를 생성한 후 부모는 자신의 실행을 재개하여 부모와 자식 스레드가 서로 독립적으로 병행하게 실행된다. 스레드가 독립적이기 때문에 스레드 사이에서 데이터 공유는 거의 없다. 비동기 스레딩은 그림 4.2에서 묘사된 다중 스레드 서버에서 사용되는 전략이다.

4.5 암묵적 스레딩

암묵적 스레딩에는 스레드가 아니라 작업을 식별하고, 언어 또는 API 프레임워크가 스레드를 만들고 관리할 수 있게 한다.

다중 코어 처리의 지속적인 성장에 따라 수백, 수천 개의 스레드를 가진 응용이 등장하게 되었다. 이런 응용을 설계하는 것은 사소한 일이 아니다. 이러한 어려움을 극복하고 병행 및 병렬 응용의 설계를 도와주는 한가지 방법은 스레딩의 생성과 관리 책임을 응용 개발자로부터 컴파일러와 실행시간 라이브러리에 넘겨주는 것이다. 암묵적 스레딩은 프로그래머가 병행 및 병렬 응용 프로그램을 개발할 때 점점 더 보편적인 기술이 되고 있다.

4.6 스레드와 관련된 문제들

4.6.3 스레드 취소

스레드 취소는 스레드가 끝나기 전에 그것을 강제 종료 시키는 작업이다. 예를 들어 여러 스레드가 데이터 베이스를 병렬로 검색하고 있다가 그 중 한 스레드가 결과를 찾았다면 나머지 스레드는 취소 되어도 된다. 이처럼 취소되어야할 스레드를 목적 스레드(target thread)라고 부른다. 목적 스레드의 취소는 다음과 같은 2가지 방식으로 할 수 있다.

비동기 취소: 스레드가 업데이트를 수행하는 중이라도 스레드를 즉시 중지한다.

지연 취소: 스레드에 종료해야 한다고 통지하지만, 스레드는 질서 정연하게 종료된다.

⇒ 대부분의 경우 비동기 종료보다 지연 취소가 선호된다.

⇒ 스레드 취소를 어렵게 만드는 것은 취소 스레드에 할당된 자원 문제이다.

⇒ 종종 운영체제는 취소된 스레드로부터 시스템 자원을 회수할 수도 있지만 모든 시스템 자원을 다 회수하지 못하는 경우도 있기 때문이다.

⇒ 따라서 비동기 종료를 하게 된다면 시스템 자원 모두를 다 사용 가능한 상태로 만들지 못할 수 있다. 반면 지연 취소를 하게 된다며 스레드가 자신이 취소 되어도 안전하다고 판단되는 시점에서 취소여부를 검사하여 취소를 시키게 된다. 따라서 스레드가 종료되기 전에 스레드가 획득한 모든 자원을 해제할 수 있다.

4.7 운영체제 사례

다른 많은 운영체제와 달리 Linux는 프로세스와 스레드를 구분하지 않는다. 대신, 각각을 태스크라고 한다. Linux clone() 시스템 콜을 사용하여 프로세스와 더 비슷하거나 스레드와 더 비슷한 태스크를 만들 수 있다.

profile
.....☆

0개의 댓글