4. Threads & Concurrency

현대적인 운영 체제들은 프로세스가 여러 스레드를 다룰 수 있게 한다. 이 장에서는 그에 대해 다룬다.

4.1. Overview

스레드는 CPU 사용의 단위로 스레드 ID와 프로그램 카운터, 레지스터 셋, 스택의 집합으로 이루어진다. 이는 코드 섹션, 데이터 섹션, 파일과 시그널 등의 운영 체제에 의한 자원들을 다른 스레드들과 공유한다. 프로세스에는 싱글 스레드멀티 스레드가 존재한다.

4.1.1. Motivation

현대적인 컴퓨터나 모바일 기기에서 작동하는 대부분의 소프트웨어는 멀티스레드이다. 이에는 썸네일 제작 앱, 웹 브라우저, 워드 프로세서 등이 있다. 이런 애플리케이션들은 멀티코어 특성을 이용할 수도 있다. 어떤 상황들에서는 단일 애플리케이션이 여러 비슷한 작업을 수행해야 할 때가 있다. 이런 경우에는 요청을 받는 별개의 단일 프로세스를 추가로 만들 수도 있지만, 복수의 스레드를 사용하는 것이 더 효율적이다. 대부분의 운영 체제 커널은 역시 멀티스레드이다. 여러 프로그램들도 복수 스레드의 이점을 누릴 수 있다.

4.1.2. Benefits

멀티스레드 프로그래밍의 이점은 크게 4가지이다.

  1. 반응성.
  2. 자원 공유.
  3. 효율성.
  4. 확장성.

4.2. Multicore programming

단일 프로세스 칩에 여러 코어를 달아 운영 체제에서 별개의 CPU로 인식하게 하는 것을 멀티코어 시스템이라 한다. 동시성 시스템은 여러 작업을 수행할 수 있게 하는 것이고 병렬화는 여러 작업을 동시에 수행하는 것이다. 즉, 병렬화 없이도 동시성을 이룰 수 있다. 싱글 코어에서는 이는 컨텍스트 스위칭으로 이루어졌다.

4.2.1. Programming Challenges

멀티코어 시스템에서의 프로그래밍의 난점은 크게 5가지이다.

  1. 작업 식별.
  2. 균형 잡기.
  3. 데이터 분할.
  4. 데이터 의존성.
  5. 테스팅과 디버깅.

이 때문에 멀티스레딩을 고려한 프로그래밍은 어렵다.

4.2.2. Types of Parallelism

병렬화에는 2가지 종류가 있는데, 하나는 같은 작업 내에서 데이터를 분산하는 데이터 병렬화이고 하나는 여러 작업을 분산하는 작업 병렬화이다. 이는 상호 배타적이지 않다.

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. Thread Libraries

스레드 라이브러리는 프로그래머에게 스레드를 만들고 관리할 수 있게 하는 API를 제공한다. 이를 구현하는 것에는 두 가지 방법이 있는데, 시스템 호출 없이 사용자 공간에서만 라이브러리를 제공하는 것과 운영 체제에서 제공하는 커널 레벨 라이브러리를 구현하는 것이 있다. 크게 POSIX pthread, 윈도우, Java 스레드 라이브러리가 있다. POSIX나 윈도우 스레드에서는 전역적으로 선언된 모든 변수는 모든 스레드간에 공유된다. Java에는 전역 데이터가 없으므로 공유 데이터에 대한 접근은 스레드간에 명시적으로 배치되어야 한다.

예제로 알아보자. 복수의 스레드를 만드는 법은 부모와 자식이 독립적으로 행동할 수 있는 비동기 스레딩과 부모가 자식 스레드들이 끝나는 것을 대기해야 하는 동기 스레딩이 있다.

4.4.1. Pthreads

Pthreads는 스레드 생성과 동기화를 정의하는 POSIX 표준 API이다. 이는 스레드 동작의 명세이지 구현이 아니다. UNIX계 시스템들은 Pthread를 구현한다. 간단한 C 프로그램 예제로 동작을 알아볼 수 있다.

4.4.2. Windows Threads

C에서 윈도우 스레드 라이브러리를 사용하는 방법도 Pthread를 사용하는 것과 비슷하다.

4.4.3. Java Threads

Java 프로그램에서 스레드를 만드는 것은 Thread 클래스로부터 파생된 클래스를 만들고 run()을 실행하는 방법과 Runnable 인터페이스를 구현하는 클래스를 정의하는 방법이 있다.

4.4.3.1. Java Executor Framework

Java에서는 Executor 인터페이스를 통해 스레드 생성이 구성될 수 있다. 이는 자바 스레드가 그 자체로는 결과를 반환할 수 없기 때문에 생긴 인터페이스로, 스레드 생성과 결과에 대한 대기를 분리시켜 준다.

4.5. Implicit Threading

멀티스레드 프로그래밍은 어렵다. 그래서 스레딩의 생성과 관리를 컴파일러와 런타임 라이브러리에 위임하는 묵시적 스레딩이 인기가 높다. 이는 프로그래머로부터 병렬화될 수 있는 작업(스레드가 아니다)을 명세하는 것을 필요로 한다.

4.5.1. Thread Pools

멀티스레딩의 첫 번째 문제는 스레드를 생성하는 데 걸리는 시간과 작업이 끝나면 스레드를 버려야 한다는 것이고, 두 번째 문제는 스레드 개수에 제한을 두지 않을 경우 시스템 자원을 소진할 수 있다는 것이다. 이것에 대한 하나의 해결책은 스레드 풀을 사용하는 것이다. 이는 다음과 같은 이점이 있다.

  1. 이미 존재하는 스레드에 대해 요청을 보내는 것은 대개 스레드 생성을 대기하는 것보다 빠르다.
  2. 스레드 풀은 스레드 개수에 제한을 건다.
  3. 스레드 생성과 작업의 수행을 분리시킨다.

스레드 풀이 제공하는 스레드 개수 제한은 휴리스틱하다. Windows API는 스레드 풀에 대한 API를 제공한다.

4.5.1.1. Java Thread Pools

Java에도 스레드 풀에 대한 API를 제공한다.

4.5.2. Fork Join

스레드 생성에 대해 지금까지 다루었던 모델은 포크-조인 모델이라고도 한다. 메인 부모 스레드가 하나 또는 그 이상의 자식 스레드를 포크하고 모든 자식 스레드들이 조인될 때까지 대기한다. 이는 스레드 풀의 동기적 버전이라고도 할 수 있다.

4.5.2.1. Fork Join in Java

Java에서는 포크-조인 스레드 라이브러리를 제공하였다. 부분 문제가 충분히 작으면 문제를 직접 풀고 그렇지 않으면 부분 문제로 분할해 스레드에 위임해 푼다. 이 포크-조인 스레드 라이브러리는 스레드간 부하 분산을 위한 작업 훔치기 알고리즘도 구현한다.

4.5.3. OpenMP

OpenMP는 C, C++, FORTRAN으로 쓰여진 프로그램에 대한 API와 컴파일러 명령어로 공유 메모리 환경에서 병렬 프로그래밍을 가능케 한다. 이는 병렬로 실행될 수 있는 병렬 지역을 식별해 병렬화 작업을 수행한다.

4.5.4. Grand Central Dispatch

GCD는 Apple에 의해 개발된 macOS/iOS용 라이브러리로서 병렬로 실행될 수 있는 코드 지역을 식별하도록 한다. 이는 작업들을 송달 큐에 넣어 제거될 때 스레드 풀의 스레드에 할당한다. 송달 큐는 연속적 큐와 동시성 큐로 나뉜다. 연속적 큐에 배치된 작업은 선입선출로 제거된다. 각 프로세스는 고유의 연속적 큐 (메인 큐)를 갖고 있다. 동시성 큐에 배치된 작업도 선입선출로 제거되지만, 여러 작업이 동시에 제거될 수 있다. 시스템 차원에서의 연속적 큐는 4가지로 나뉜다.

  • QOS_CLASS_USER_INTERACTIVE. 유저와 상호작용하는 클래스.
  • QOS_CLASS_USER_INITIATED. 유저에 의해 시작되는 클래스.
  • QOS_CLASS_UTILITY. 유틸리티 클래스.
  • QOS_CLASS_BACKGROUND. 백그라운드 클래스.

송달 큐에 할당되는 작업은 2가지로 나뉜다.

  1. C, C++, Objective-C 언어들에서는 GCD는 언어에 대한 블록을 식별한다.
  2. Swift 언어에서는 작업은 클로저를 통해 정의된다.

GCD 스레드 풀은 POSIX 스레드로 구성되어 있다.

4.5.5. Intel Thread Building Blocks

인텔의 TBB는 C++에서 병렬 작업을 할 수 있게 하는 템플릿 라이브러리이다. 이는 parallel_for 명령어 안에 반복 공간과 작업을 할당한다.

4.6. Threading Issues

멀티스레드 프로그램에는 여러 이슈가 있다.

4.6.1. The fork() and exec() System Calls

fork()가 모든 스레드를 복제할지 fork()를 호출한 스레드만 복제할지는 어떤 fork()를 쓰냐에 따라 다르므로 상황에 따라 잘 선택해야 한다.

4.6.2. Signal Handling

시그널은 UNIX 시스템에서 프로세스에 특정 이벤트가 발생했음을 알리는 도구이다. 이는 특정 이벤트가 발생할 시 생성되어 프로세스에 전달된다. 이 경우 반드시 시그널이 핸들링되어야 한다. 동기 시그널은 잘못된 메모리 접근이나 0으로 나누기 등이다. 비동기 시그널은 프로세스 외부에서 전달되는 시그널로, Ctrl+C에 의한 프로세스 종료 등이다. 시그널은 2가지 핸들러로 다뤄질 수 있다.

  1. 기본 시그널 핸들러.
  2. 사용자 정의 시그널 핸들러.

모든 시그널은 기본 시그널 핸들러를 가지고 이는 사용자 정의 시그널 핸들러로 오버라이딩될 수 있다. 멀티스레드 프로그램에서는 시그널 전달에 다음 방법들이 쓰인다.

  1. 시그널이 적용되는 스레드에 전달한다.
  2. 시그널을 모든 스레드에 전달한다.
  3. 특정 스레드에만 시그널을 전달한다.
  4. 시그널을 받는 것을 전담하는 스레드를 둔다.

이는 발생한 시그널의 유형에 따라 다르다. 예를 들어 동기 시그널은 시그널을 발생시킨 스레드에만 전달시켜야 한다. 비동기 시그널은 그렇지는 않다. 그래서 POSIX은 pthread_kill 등의 API를 제공한다. Windows에서는 비동기 프로시져 호출(APCs)로 이를 흉내낸다.

4.6.3. Thread Cancellation

스레드 취소는 스레드가 끝나기 전에 그를 중지시키는 것이다. 중지될 스레드는 대상 스레드라고도 한다. 이 스레드의 취소는 2개의 시나리오에서 발생할 수 있다.

  1. 비동기 취소. 한 스레드가 대상 스레드를 즉시 취소함.
  2. 지연된 취소. 대상 스레드가 자신이 정지되어야 하는지를 주기적으로 체크함.

이것이 문제가 되는 경우는 대상 스레드에 자원이 할당되었을 때나, 대상 스레드가 여러 스레드간 공유되는 데이터를 업데이트하는 도중에 중지될 떄 발생한다. 이는 비동기 취소일 때 더 문제가 된다. 그래서 비동기 스레드 취소는 시스템 차원의 자원을 해제하면 안 된다. 이에 비해 지연된 스레드 취소는 안전성 체크를 할 수 있다. Pthreads는 기본적으로는 지연된 취소를 사용하며, 스레드가 취소 지점에 도달했을 때만 취소가 발생한다. Pthreads는 스레드가 취소될 때 실행될 수 있는 클린업 핸들러를 가능케 한다. 이는 얻은 자원을 해제하는 데 쓰인다. Java 스레드 취소의 메커니즘도 Pthreads와 비슷하다.

4.6.4. Thread-Local Storage

어떤 환경에서는 각 스레드가 특정 데이터의 복제를 필요로 할 수도 있다. 이런 데이터를 스레드 국지적 저장소(TLS)라 한다. 이는 static 데이터와 비슷하지만, 스레드 각각에 대해 유일하다는 차이점이 있다.

4.6.5. Scheduler Activations

멀티스레드 프로그램에서 고려되어야 하는 마지막 이슈는 커널과 스레드 라이브러리간 통신이다. 다대다 모델을 구현하는 많은 시스템은 사용자 스레드와 커널 스레드간 중간 자료 구조로 경량 프로세스(LWP)를 구현한다. 사용자 스레드 입장에서 이것은 가상 프로세서로 여겨진다.

사용자 스레드 라이브러리와 커널간 통신하는 하나의 방법은 스케쥴러 활성화이다. 커널은 애플리케이션과 가상 프로세서를 제공하고, 애플리케이션은 사용자 스레드를 가용가능한 가상 프로세서에 스케쥴링한다. 이 때 커널은 애플리케이션에 대해 특정 이벤트를 공지해야 하는데 이를 업컬이라 한다. 이는 업컬 핸들러에 의해 다뤄진다. 애플리케이션 스레드가 블록될 때 업컬이 일어나고는 한다.

4.7. Operating-System Examples

운영 체제별 예제를 알아보자.

4.7.1. Windows Threads

윈도우 스레드는 대개 다음으로 이루어진다.

  • 스레드 ID.
  • 레지스터 집합.
  • 프로그램 카운터.
  • 사용자 스택과 커널 스택.
  • 런타임 라이브러리와 동적 링크 라이브러리에 의해 사용되는 사적 저장소.

레지스터 집합, 스택, 사적 저장소 영역을 스레드의 컨텍스트라 한다. 스레드의 자료 구조는 다음을 포함한다.

  • ETHREAD, 실행되는 스레드 블록
  • KTHREAD, 커널 스레드 블록
  • TEB, 스레드 환경 블록

ETHREAD와 KTHREAD는 커널 공간에 존재한다. TEB는 사용자 공간 자료 구조로 사용자가 사용자 모드에서 동작할 때 접근된다. 이는 스레드 국소적 저장소를 포함한다.

4.7.2. Linux Threads

Linux는 프로세스와 스레드를 구별하지 않는다. 대신에 작업이라는 단위를 쓴다. Linux 시스템은 clone()으로 스레드를 생성할 수 있게 한다. 이가 실행될 때 부모와 자식 작업간에 자원들이 얼마나 공유되는지를 결정하게끔 할 수 있다.

4.8. Summary

  • 스레드는 CPU 활용의 기본적 단위이다. 같은 프로세스에 속하는 스레드들은 코드와 데이터를 포함한 프로세스 자원을 공유한다.
  • 멀티스레드 프로그래밍에는 4가지 이점이 있다. 반응성, 자원 공유, 효율성, 확장성.
  • 복수의 스레드가 작업을 하는 것을 동시성이라 하고 복수의 스레드가 동시에 작업을 하는 것을 병렬화라 한다. 단일 CPU가 있는 시스템에서는 동시성만이 가능하다. 병렬화는 다중 코어 시스템을 필요로 한다.
  • 멀티스레드 프로그램을 설계하는 데에는 여러 난점이 있다. 작업을 배분 및 균형 맞추는 것, 스레드간 데이터의 분할, 데이터의 의존성 해결. 테스팅과 디버깅도 어렵다.
  • 데이터 병렬화는 코어 내에서 같은 동작을 수행하며 데이터를 여러 코어에 분산한다. 작업 병렬화는 여러 코어에 작업을 분산하며 각 작업은 고유의 동작을 수행한다.
  • 사용자 애플리케이션은 사용자 레벨 스레드를 생성하며 이는 커널 스레드에 매핑되어야 한다. 다대일 모델은 여러 사용자 레벨 스레드를 하나의 커널 스레드에 매핑한다. 일대일이나 다대다 모델도 있다.
  • 스레드 라이브러리는 스레드를 생성하고 관리하는 API를 제공한다. 3개의 널리 쓰이는 스레드 라이브러리는 Windows, Pthreads, Java 스레딩이다. 윈도우는 윈도우 시스템에서, Pthreads는 POSIX 호환 가능한 시스템에서, Java 스레드는 JVM을 지원하는 시스템에서 가동된다.
  • 묵시적 스레딩은 작업을 특정하고 (스레드가 아니다) 언어나 API 프레임워크가 스레드를 생성하고 관리하도록 한다. 스레드 풀, 포크-조인 프레임워크, GCD 등의 접근법이 있다. 이는 매우 널리 쓰이고 있다.
  • 스레드는 비동기적으로 또는 지연된 식으로 종료될 수 있다. 비동기 취소는 스레드가 업데이트 작업 중이더라도 즉시 중단시킨다. 지연된 중단은 스레드에 중지되어야 함을 통지하나 스레드가 순서화된 방식으로 종료될 수 있도록 한다. 대개 지연된 중단이 선호된다.
  • Linux는 스레드와 프로세스를 구분하지 않으며, 각각을 작업으로 여긴다. clone()은 프로세스나 스레드처럼 행동할 수 있는 작업을 생성할 수 있다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중