19 Threads

7 minute read

Threads

같은 프로그램을 세 번 실행하게 되면

기존의 시스템 구조 (single-threaded process) 에서는 프로세스를 세 개를 만들게 될 것이다.

그러나 이렇게 하지 않고, 어차피 같은 프로그램이라면 코드는 같을테니

프로세스 내에서 같은 프로세스가 실행이 된다면

스레드라는 것을 만들어서 각자 역할을 나누고, 코드와 데이터를 공유하게 하면

더 효율적인 동작을 기대할 수 있다.

 

image

 

위와 같이 multithreaded process 에서는

thread가 프로세스 안에 추가되는 형식이다.

code와 data는 스레드들이 공유하게 된다.

multithreaded process 는 프로그램의 기준이 thread가 되고

CPU scheduler 는 이제 process 가 아닌 thread를 대상으로 작동한다.

 

 

Why?

대부분의 현대적인 응용 프로그램은 모두 멀티스레드로 동작한다.

스레드들은 응용 프로그램 안에서 동작하게 된다.

하나의 응용 프로그램은 여러 실행 흐름을 가진 독립적인 thread들로 구현된다는 뜻이다.

응용 프로그램에서 다수의 작업은 개별적인 스레드들로 분할되어 구현된다.

특히 스레드를 생성하는 것은 프로세스 하나를 만드는 것보다 쉽다.

때문에 프로세스의 생성은 heavy-weight 하고, 스레드의 생성은 light-weight 하다.

같은 이유로 스레드를 light weight process 라고 부르기도 한다.

스레드는 코드를 단순화하고 효율성을 높일 수 있다.

커널은 보통 멀티 스레드로 동작한다.

 

 

Multithreaded Server Architecture

 

image

 

여기서 client 는 웹 브라우저를 의미한다.

웹 브라우저가 서버에 요청을 보내면 서버는 그 요청을 받아서

그 요청을 처리하기 위한 스레드를 생성한다.

따라서 서버는 요청을 받는 스레드와, 요청을 처리하는 스레드들로 구성된다.

 

 

스레드 사용의 이익

대화형의, interactive 한 응용 프로그램의 경우, 응용 프로그램의 일부가 block 되거나

긴 작업을 수행하다가도, 프로그램의 다른 부분이 수행되도록 허용할 수 있다.

사용자에게 결과적으로 **응답성 (responsiveness) **을 증가시키는 결과를 불러온다.

 

스레드들은 프로세스의 자원을 공유하고

이는 공유 메모리나 메시지 패싱보다 더 쉽다.

 

프로세스 생성, 스레드 스위칭은 컨텍스트 스위칭(프로세스 전환) 보다 \

시간이 더 적게 들고 적은 비용이 드므로 경제적 이다.

 

프로세스는 멀티프로세서 구조에서 이득을 볼 수 있다.

싱글 스레드였다면 멀티프로세서를 줘도 CPU 하나만 쓸 수 있는 반면

멀티 스레드는 병렬적으로 실행이 가능하므로 확장성 이 있다.

 

 

Multicore Programming

단일 칩 안에 컴퓨팅 코어가 여러 개인 것을

Multicore, 칩이 여러 개 있는 것을 Multiprocessor 라고 한다.

이들은 병렬성(parallelism)동시성(concurrency) 를 가진다.

 

  • 병렬성은 시스템이 하나 이상의 작업을 동시에 시행할 수 있는 것을 의미한다.

  • 동시성은 하나 이상의 작업이 진행이 됨을 의미한다.

    이는 싱글 프로세서/싱글 코어에서도 가능하다.

 

 

병렬성은 데이터 병렬성작업 병렬성 으로 나뉜다.

데이터 병렬성 (Data Parallelism)

전체 데이터 집합은 다수의 데이터 집합으로 나뉘게 되고

이들은 다시 다수의 컴퓨팅 코어에 할당된다.

그 코어에서는 동일한 연산을 실행한다.

 

예를 들어 수 100개를 더하는 작업이 있다고 할 때,

1번 코어는 1~50, 2번 코어는 51~100까지를 더하는 작업을 할 수 있다.

이렇게 하면 수행 시간은 한 코어가 1~100을 다 더하는 것보다 반 정도 줄어든다.

 

작업 병렬성 (Task Parallelism)

각 스레드가 그 고유의 일을 수행하는 것이다.

1번 코어는 수의 합을 수하고, 2번 코어는 그 수들 중 가장 큰 수를 찾는 작업을 할 수 있다.

 

이 두 종류의 병렬성은 프로그래머들에게 다음과 같은 도전과제를 내민다.

 

  • dividing activities

    응용을 분석 후, 그 응용이 서로 독립된 작업으로 나뉠 수 있는지 따져보아야 한다.

  • balance

    병렬 실행이 가능하도록 나누었을 때 균등한지 알아보아야 한다.

    한 쪽은 10개만 더하고 한 쪽은 90개를 더하면 옳지 않다.

  • data splitting

    데이터를 나누어 주어야 한다.

  • data dependency

    task 1이 데이터를 바꾸면 task 2에 영향을 미치면 안 된다.

    데이터 의존성을 줄여야 한다.

  • testing and debugging

    병렬 실행을 하게 되면 다양한 실행 경로가 존재하게 된다.

    이들이 다 올바르게 동작하는지 검증해야 한다.

 

 

암달의 법칙

amdahl

 

위 식은 순차 실행해야 하는 구성 요소와

병렬 실행해야 되는 구성 요소를 동시에 가지고 있을 때

이 응용을 멀티 코어에서 실행했을 때 얻을 수 있는 성능 향상을 나타낸다.

 

S 는 순차 실행해야 하는 부분의 비율이고 N 은 코어의 개수이다.

 

만일 한 응용 프로그램에서 75%가 병렬이고 25%가 순차 실행해야 하며

코어 1개 짜리에서 코어 2개짜리로 옮기게 된다면

S = 0.25 이므로 1/(0.25 + 0.75/2) = 1.6 배 정도 성능이 향상되게 된다.

 

N이 무한정 많아지게 되면 성능 향상은 1/S 가 된다.

즉 만약 S가 40%라면 아무리 코어를 올려도 2.5배 이상 올릴 수 없다는 뜻이다.

응용 프로그램에 포함되는 순차 실행은

코어를 추가해서 얻을 수 있는 성능 향상에 불균형적인 영향을 끼친다.

이 말은 코어를 늘리면 성능은 올라가겠지만, 물리적인 한계가 있음을 의미한다.

즉 상한선이 존재한다는 뜻이다.

 

하지만 요즘의 멀티코어 시스템에 항상 맞는 말이라는 보장은 없고

이견들이 많이 존재한다.

 

 

멀티스레딩 모델들

멀티 스레드는 user threadskernel threads 로 나뉜다.

user thread 는 사용자 수준에서 라이브러리를 통해 접근이 가능한 스레드이다.

이 라이브러리로 하위 운영 체제 커널에서 스레드 기능을 제공하는지 안 하는지에 관계 없이

스레드를 사용 가능(생성, 종료 등) 하다.

 

이 라이브러리에는 POSIX Pthreads, Windows Thread, Java Threads 등이 있다.

 

kernel threads 는 커널이 지원하는 스레드이다.

Windows, Linux, MacOS X, Solaris 등 범용적인 운영 체제들이

이 유저 스레드와 커널 스레드를 지원한다.

 

이때 이 유저 스레드와 커널 스레드는 독립적인 것이 아니라

각각 연관을 갖게 된다.

예를 들어 커널 스레드를 지원하는 리눅스 운영 체제에서

Pthreads 라이브러리로 유저 스레드를 만들어 사용하는 경우

내가 만든 유저 스레드에 대응하는 커널 내의 커널 스레드가 반드시 하나 존재한다.

시스템에 따라 미리 정해놓은 유저 스레드 - 커널 스레드 간의 관계가 있다.

이 유형은 Many to One, One to One, Many to Many 등이 있다.

 

Many to One

image

 

많은 수의 유저 수준의 스레드들이 하나의 커널 스레드로 매핑되는 방식이다.

이때 하나의 스레드가 블록 되면 모든 스레드가 블록된다.

그리고 이게 말만 멀티스레드이지, 하나에 한 스레드만 작동하기 때문에

멀티코어 시스템에서 병렬적으로 돌아갈 수가 없다.

요즘은 몇 안 되는 시스템이 이 방식을 차용한다.

Solaris Green Threads, GNU Portable Threads 등이 이 방식을 사용한다.

 

 

One to One

image

 

각 사용자 스레드가 하나의 커널 스레드에 매핑 된다.

유저 스레드가 하나 만들어지면 커널 스레드도 만들어진다.

many to one 보단 동시성을 더 가지게 된다.

하지만 이게 많아지면 안 되므로

사용자 수준에서 만들 수 있는 스레드 개수를 정해놓기도 한다.

장점은 병렬 실행이 가능하며

단점은 너무 많은 스레드 사용에 주의해야 한다.

Windows, Linux, Solaris 9 버전 이상이 이 방식을 사용한다.

 

 

Many to Many

image

 

많은 수의 유저 스레드가 많은 수의 커널 스레드와 매핑된다.

운영 체제가 충분한 개수의 커널 스레드를 생성할 수 있도록 한다.

이는 Many to One 의 단점인 병렬성의 불가와

One to One 의 단점인 스레드 과잉을 보완한 모델이다.

따라서 커널 스레드는 유저 스레드의 수보다 작거나 같다.

또한 이 연결은 multiplex 하여,

어느 스레드가 어느 스레드와 연결될지는 그때마다 다르다.

Solaris 9 이전 버전과 Windows wiht the ThreadFiber package 가 이 모델을 사용한다.

 

 

Two-level model

image

 

기본적으로 Many to Many 방식을 사용하되

일부 스레드는 One to One 을 혼용하는 방식이다.

IRIX, HP-US, Tru64 UNIX, Solaris 8 버전 이전이 이 방식을 사용한다.

 

 

Thread Libraries

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

이는 라이브러리가 오롯이 사용자에게 맡겨지는 경우도 있고

운영 체제가 지원하는 커널 수준의 라이브러리일 수도 있다.

POSIX Pthreads, Windows Threads, Java Thread 등이 있다.

 

 

Pthreads

사용자 수준, 커널 수준에서 제공된다.

IEEE 1003.1c 표준을 지키는 라이브러리이다.

IEEE 1003.1c 는 스레드의 생성과 동기화 방법에 대한 표준을 정의한 것이다.

이는 구현해야 하는 원칙을 정해놓은 것이지 어떻게 구현하는지는 라이브러리의 개발자에 달렸다.

UNIX 운영 체제에서 흔하다. (Solaris, Linux, Mac OS X)

 

 

Implicit Threading

스레드의 수가 많아지면서 프로그램을 검증하는 것이 어려워졌다.

하나의 프로그램이 수 백개의 스레드를 생성하기도 하는데

이렇게 스레드 수가 많으면 검증하는 것이 어렵다.

따라서 이를 해결하기 위해 스레드의 생성과 삭제를 사용자가 하는 게 아니라

컴파일러에게 맡겨버릴 수가 있다.

이 방식에는 Thread Pools, OpenMP, Grand Central Dispatch 등이 있다.

 

 

Thread Pools

스레드가 무한정 늘어나는 것을 방지하기 위한 기법이다.

프로세스 시작 시 일정 수의 스레드를 Pool 형태로 만들어놓고 대기시킨다.

그리고 스레드에 요청이 들어오면 그때 작업을 시작한다.

이는 보통 스레드를 그때 그때 만드는 것보다 빠르다는 장점이 있다.

그리고 풀의 크기는 제한된다.

task 의 실행을 생성과 분리하여서 실행에 대해 다양한 전략을 적용할 수 있다.

예를 들어 task 들이 주기적으로 스케쥴링 될 수 있다.

 

 

OpenMP

C, C++, Fortran 언어들을 위한 컴파일러 directives 이다.

공유 메모리 환경에서의 병렬 프로그래밍을 지원한다.

OpenMP에서 병렬로 실행 가능한 부분을 Parallel regions 라고 하는데

개발하다가 자신의 코드 중에 Parallel region 에 해당하는 부분에

OpenMP directives 를 삽입하면

해당 영역을 병렬로 실행하도록 라이브러리에 요청할 수 있다.

 

 

Grand Central Dispatch

애플이 Mac OS X, iOS 운영 체제를 위해 개발한 기술이다.

이는 C, C++ 언어, API와 런타임 라이브러리를 확장한 것이다.

GCD는 ^{} 로 블록을 정의한다.

이 블록은 dispatch queue에 들어가는데, 나중에 여기서 블록을 빼서 실행한다.

블록을 빼서 해당 블록을 스레드 풀에서 꺼낸 가용 스레드에 할당해서 실행한다.

Parallel sections 를 알아볼 수가 있다.

대부분의 스레드 동작을 알아서 수행한다.

 

dispatch queue 에는 두 가지 방법이 있는데

dispatch queue 는 GCD 의 핵심이다.

첫째로 serial dispatch queue 는 FIFO 순으로 동작한다.

먼저 들어간 블록이 먼저 나오며

꺼내진 블록은 또 다른 블록이 실행되기 전에 실행되는 것이 보장되어 있다.

이 큐는 프로세스마다 하나씩 가지고 있으며

이는 main queue라고도 불린다.

 

concurrent dispatch queue 는 FIFO 순인 것은 동일하지만

한 번에 여러개를 꺼내서 동시에 실행할 수도 있다. 이는 시스템 전체에 딱 세 개가 있다.

이 세 개의 큐는 우선순위에 따라 low, default, high 로 나뉜다.

중요도에 따라서 적절히 배치해야 한다.

 

 

threading issues

스레드 사용에는 이전의 한 프로세스만 다루었을 때와 달리

다음과 같은 질문이 따른다.

  • fork() 사용 시에, 전체 스레드를 다 복사할 것인지,

    이를 호출한 스레드만 복사할 것인지를 정해야 한다.

  • 사건이 발생하면 시그널을 보내야 하는데

    이걸 어떤 스레드한테 전달할지 알아야 한다.

  • 하나가 더 이상 필요없어지게 되면 스레드를 중단(thread cancellation)해야 하는데,

    이때 생길 수 있는 문제들

  • thread-local storage

    이는 지역변수와 다르다. 함수의 시작 전이나 종료 후,

    호출 후에도 접근할 수 있는 스레드만의 전용 공간이다.

    이를 어떻게 다룰지도 고민해보아야 한다.

  • scheduler activations

    이제 프로세스가 아니라 스레드가 scheduling 의 대상이 된다.

Updated: