Sun Solaris 멀티 쓰레드의 개념
 
<표 1>은 이 글에서 사용되는 몇 가지 용어를 소개하고 있다.
 
표 1 > 멀티 쓰레딩 용어
용어 정의
 
프로세스(process)

프로그램을 실행하기 위해 설정된 fork (2) 시스템 호출을 통해 생성된 UNIX 환경(파일 기술자, 사용자 ID 등)

쓰레드(thread) 프로세스 컨텍스트 내에서 실행되는 명령어 시퀀스
p쓰레드(POSIX 쓰레드) POSIX 1003.1c 호환 쓰레드 인터페이스
Solaris 쓰레드(Solaris thread) POSIX와 호환되지 않는 썬 마이크로시스템즈 쓰레드 인터페이스
단일 쓰레딩(single-threaded) 단일 쓰레드 액세스로 제한
멀티 쓰레딩(multithreaded)

2개 이상의 쓰레드에 대한 액세스 지원. 사용자(커널에 반대되는) 공간의 쓰레드

사용자 또는 애플리케이션 레벨 쓰레드
(User- or Application-level thread)
라이브러리 루틴에 의해 관리되는 쓰레드
경량형 프로세스(lightweight process)

커널 코드 및 시스템 호출을 실행하는 커널의 쓰레드 (LWP라고도 불림)

바운드 쓰레드(bound thread) LWP에 영구적으로 연결되는 쓰레드
언바운드 쓰레드(unbound thread)

커널 지원 없이 매우 빠르게 컨텍스트 스위칭을 수행하는 디폴트 Solaris 쓰레드

속성 객체(attribute object)

POSIX 쓰레드, 상호 배제 락(mutual exclusion lock, mutexe) 및 조건 변수의 구성 가능한 측면을 표준화하기 위해 사용되는 데이터 타입 및 관련 조작 기능 포함

상호 배제 로크(mutual exclusion lock)

공유 데이터에 대한 액세스 락(lock) 및 언락(unlock)을 지원하는
기능

조건 변수(condition variables) 상태 변화 때까지 쓰레드를 차단(blocking) 하는 기능
카운팅 세마포어(counting semaphore) 메모리 기반의 동기화 메커니즘
패러럴리즘(parallelism) 최소 2개의 쓰레드가 동시에 실행되고 있을 때 발생하는 상황
동시성(concurrency)

최소 2개의 쓰레드가 진행될 때 존재하는 상황. 가상 패러럴리즘의 한 형태인 시간 분할(time slicing) 기능을 포함하고 있는 보다 일반적인 형태의 패러럴리즘.

 

멀티 쓰레딩 프로그래밍의 개념은 최소한 1960년 대로 거슬러 올라간다. UNIX 시스템 상에서의 멀티 쓰레딩 프로그램 개발은 1980년대 중반부터 시작됐다. 멀티 쓰레딩에 대한 정의와 이를 지원하는데 필요한 기능에 대한 합의가 이루어졌지만, 멀티 쓰레딩 구현에 사용되는 인터페이스는 매우 다양했다.
수년 간 POSIX(Portable Operating System Interface) 1003.4a라 불리는 그룹이 멀티 쓰레딩 프로그래밍을 위한 표준 개발에 주력했다. 현재, 이 표준은 승인을 받은 상태이다. 이 글은 POSIX 표준인 P1003.1b 최종 드래프트 14(실시간) 및 P1003.1c 최종 표준 10(멀티 쓰레딩)을 기초로 작성됐다.
또 여기서는 POSIX 쓰레드(p쓰레드라고도 불림) 및 Solaris 쓰레드를 모두 다루고 있다. Solaris 쓰레드는 Solaris 2.4 버전부터 지원되며, POSIX 쓰레드와 기능 측면에서 크게 차이나지 않는다. 한편, 이 글에서는 POSIX 쓰레드가 Solaris 쓰레드에 비해 이식성이 훨씬 뛰어나다는 사실을 감안, POSIX 관점에서 멀티 쓰레딩에 대해 알아보고자 한다.

 

> 애플리케이션 응답성 개선
많은 작업이 상호 의존적으로 수행되지 않는 프로그램의 경우, 이를 재설계해, 각 활동을 쓰레드로 정의할 수 있다. 예를 들어, 멀티 쓰레딩 GUI 사용자는 하나의 작업이 완료될 때까지, 다른 작업에 착수하는 것을 미룰 필요가 없다.

 

> 효율적인 멀티프로세서 활용
일반적으로, 쓰레드 실행시 동시성을 요구하는 애플리케이션은 가용 프로세서의 수를 고려할 필요가 없다. 애플리케이션 성능은 프로세서 추가를 통해 투명하게 개선된다.
행렬 곱셈(matrix multiplication)과 같이 패러럴리즘의 수준이 높은 수치 알고리즘 및 애플리케이션은 멀티프로세서 상의 쓰레드로 구현될 때, 훨씬 빠르게 실행될 수 있다.

 

> 프로그램 구조 개선
많은 프로그램을 단일 모놀리식(monolithic) 쓰레드가 아니라 여러 개의 독립적 또는 중간 독립적(semi-independent) 실행 유닛으로 구성함으로써 효율성을 높일 수 있다. 멀티 쓰레딩 프로그램은 단일 쓰레딩 프로그램보다 다양한 사용자 요구에 대한 보다 뛰어난 적응성을 갖게 된다.

 

> 최소한의 시스템 자원 활용
공유 메모리를 통해 공통의 데이터에 액세스하는 2개 이상의 프로세스를 사용하는 프로그램은 1개 이상의 제어 쓰레드를 적용하고 있다.
그러나, 각 프로세스는 완벽한 어드레스 공간 및 운영 환경 상태를 보유하고 있다. 대용량의 상태 정보를 작성 및 유지보수 하는데 소요되는 비용 때문에, 각 프로세스는 시간 및 공간 측면에서 쓰레드 보다 많은 비용을 소모하게 된다.
뿐만 아니라,프로세스 간의 구분 속성 때문에 프로그래머들은 서로 다른 프로세스 내 쓰레드 간 통신이나 그 실행을 동기화하는데 상당한 노력을 기울여야 한다.

> 쓰레드와 RPC의 결합
쓰레드와 RPC(Remote Procedure Call) 패키지를 결합함으로써, 비공유 메모리 멀티프로세서(예를 들면 워크스테이션 컬렉션과 같은)의 이점을 활용할 수 있다. 이러한 결합을 통해, 비교적 손쉽게 애플리케이션을 배포하는 것은 물론, 워크스테이션 컬렉션을 멀티프로세서로서 다룰 수 있게 된다.
예를 들면, 하나의 쓰레드로 하위(child) 쓰레드를 생성할 수 있다. 이들 각각의 하위 쓰레드는 원격 프로시저를 요청해, 다른 워크스테이션 상에서 프로시저를 호출하게 된다. 오리지널 쓰레드는 현재 병렬로 실행되고 있는 쓰레드만을 생성하지만, 다른 컴퓨터에서도 패러럴리즘이 구현된다.

 

> 동시성 및 패러럴리즘
단일 프로세서 상의 멀티 쓰레딩 프로세스의 경우, 해당 프로세서는 쓰레드 간의 실행 자원을 교환할 수 있기 때문에, 동시 실행이 이루어지게 된다.
공유 메모리 멀티프로세서 환경 내 동일한 멀티 쓰레딩 프로세스의 경우, 해당 프로세스 내에서 각 쓰레드는 별도의 프로세서 상에서 동시에 실행됨으로써 병렬 실행이 이루어지게 된다. 해당 프로세스에서 쓰레드 수가 프로세서 수와 같거나 이보다 적을 경우, 해당 운영 환경과 함께 쓰레드 지원 시스템은 각 쓰레드가 서로 다른 프로세서 상에서 작동할 수 있도록 보장하게 된다. 예를 들면, 쓰레드와 프로세서 수가 동일한 행렬 곱셈에서 각 쓰레드(및 각 프로세서)는 결과의 1 행(row)을 계산하게 된다.

 

> 멀티 쓰레딩 구조의 이해
기존의 UNIX도 이미 쓰레드 개념을 지원하고 있다. 즉 각 프로세스는 하나의 쓰레드를 포함하고 있기 때문에, 멀티프로세스를 이용한 프로그래밍은 멀티 쓰레드를 통해 수행된다. 그러나, 하나의 프로세스는 하나의 어드레스 공간을 의미하기 때문에, 하나의 프로세스를 구축한다는 것은, 하나의 새로운 어드레스 공간을 생성하는 것이 된다.
새롭게 생성된 쓰레드는 기존의 프로세스 어드레스 공간을 사용하기 때문에, 쓰레드를 만드는 것이 새로운 프로세스를 만드는 것보다 훨씬 저렴하다. 쓰레드 간 스위칭의 경우, 어드레스 공간의 스위칭을 포함하지 않기 때문에 쓰레드 간의 스위칭에 소요되는 시간은 프로세스 스위칭에 소요되는 시간보다 훨씬 적다.
쓰레드는 모든 것, 특히 어드레스 공간을 공유하기 때문에 단일 프로세스 내 쓰레드간 통신은 간단하다. 따라서, 하나의 쓰레드가 생성한 데이터는 다른 모든 쓰레드에서 즉시 사용할 수 있게 된다.
멀티 쓰레딩에 대한 인터페이스 지원은 POSIX 쓰레드의 경우 libpthread, Solaris 쓰레드의 경우 libthread 같은 서브루틴 라이브러리를 통해 이루어진다. 멀티 쓰레딩은 커널 레벨 및 사용자 레벨 자원을 분리함으로써 유연성을 부여하게 된다.

 

> 사용자 레벨 쓰레드
쓰레드는 멀티 쓰레딩 프로그래밍의 주요 프로그래밍 인터페이스이다. 사용자 레벨 쓰레드1는 사용자 공간에서 처리되며, 커널 컨텍스트 스위칭 과정을 배제하게 된다. 하나의 애플리케이션이 수백 개의 쓰레드를 가질 수 있으며, 여전히 많은 커널 자원을 소비하지 않는다. 애플리케이션이 사용하는 커널 자원의 크기는 주로 애플리케이션에 의해 결정된다.
쓰레드는 어드레스 공간, 개방된 파일 등과 같은 모든 프로세스 자원을 공유하고 있는 프로세스 내에서만 볼 수 있다. 아래와 같은 상태는 각 쓰레드에 따라 고유하게 지정된다.

 
쓰레드 ID
레지스터 상태 (PC 및 스택 포인터 포함)
스택
시그널 마스크(signal mask)
우선 순위
쓰레드 전용(thread-private) 스토리지
 

쓰레드는 프로세스 명령어와 프로세스 데이터의 대부분을 공유하기 때문에 한 쓰레드에 의해 공유 데이터 내에 변경이 이루어질 경우, 해당 프로세스 내의 다른 쓰레드가 이를 확인할 수 있다. 한 쓰레드가 동일한 프로세스 내의 다른 쓰레드와 상호 작용해야 할 경우, 운영 환경의 개입없이 이를 수행할 수 있다.
쓰레드는 기본값으로 초경량(lightweight)으로 설정되어 있다. 그러나, 쓰레드를 보다 완벽하게 제어하기 위해(예를들어, 스케줄링 정책을 보다 완벽하게 제어하기 위해) 애플리케이션은 이 쓰레드를 연결(binding)할 수 있다. 애플리케이션이 쓰레드를 실행 자원에 연결하면 쓰레드는 커널 자원이 된다.
사용자 레벨 쓰레드에 대한 내용을 요약하면 다음과 같다.

 

고유한 어드레스 공간을 생성할 필요가 없기 때문에, 보다 저렴한 비용으로 개발할 수 있다. 이는 런타임 시 어

 

드레스 공간에서 할당되는 가상 메모리이다.

커널 레벨이 아니라 애플리케이션 레벨에서 수행되기 때문에 신속하게 동기화할 수 있다.
libpthread 또는 libthread와 같은 쓰레드 라이브러리를 통해 손쉽게 관리할 수 있다.
 

> 경량형 프로세스
쓰레드 라이브러리는 커널에 의해 지원되는 경량형 프로세스라고 불리는 기본 제어 쓰레드를 사용한다. 이러한 LWP는 코드나 시스템 요청을 실행하는 가상 CPU로서 간주할 수 있다.
쓰레드를 이용해 프로그래밍하는데 있어, LWP 때문에 고민할 필요는 없다.


주 - Solaris 2, Solaris 7 및 Solaris 8 운영 환경의 LWP는 Solaris 2, Solaris 7 및 Solaris 8 운영 환경에서 지원되지 않는 SunOS 4.0 LWP 라이브러리의 LWP와는 다른 것이다.

 

fopen() 및 fread() 같은 studio 라이브러리 루틴이 open() 및 read() 함수를 사용하는 것처럼, 상당 부분 같은 이유로 쓰레드 인터페이스도 LWP 인터페이스를 사용하고 있다.
LWP(Lightweight Process)는 사용자 레벨과 커널 레벨을 잇는 가교 역할을 한다. 각 프로세스는 1개 이상의 LWP와 바인딩을 포함하고 있으며, 각 LWP는 1개 이상의 사용자 쓰레드를 실행하고 있다(<그림 1> 참조). 쓰레드를 생성하기 위해서는 일부 사용자 컨텍스트의 작성이 이루어져야 하지만, LWP의 생성과는 전혀 관계가 없다.

각 LWP는 커널 풀(pool) 내 커널 자원으로서, 각 쓰레드 단위로 할당(attached) 또는 해제(detached) 된다. 이는 쓰레드가 스케줄링 및 소멸될 때 발생하게 된다.

> 스케줄링
POSIX는 FIFO(first-in-first-out)(SCHED_FIFO), 라

운드 로빈(round-robin)(SCHED_RR) 및 맞춤형(SCHED_OTHER) 등 3가지 스케줄링 정책에 대해 구체적으로 명시하고 있다. SCHED_FIFO는 각 우선순위 레벨에 따라 서로 다른 대기 행렬을 가진 대기 행렬 기반의 스케줄러이다. SCHED_RR은 각 쓰레드가 할당된 실행 시간을 가지고 있다는 점을 제외하고는 FIFO와 유사하다.
SCHED_FIFO 및 SCHED_RR 모두 POSIX Realtime 익스텐션이다. SCHED_OTHER는 기본값으로 설정된 스케줄링 정책이다.
언바운드 쓰레드를 위한 프로세스 범위와 바운드 쓰레드를 위한 시스템 범위 등 2가지 스케줄링 범위가 지원된다. 서로 다른 범위 상태를 지닌 여러 쓰레드들이 동일한 시스템 상은 물론, 심지어 동일한 프로세스 상에 공존할 수 있다. 일반적으로 범위는 쓰레드 스케줄링 정책이 유효한 범위를 설정하게 된다.

 

> 프로세스 범위(언바운드 쓰레드)
언바운드 쓰레드는 PTHREAD_SCOPE_PROCESS으로 생성된다. 이들 쓰레드는 LWP 풀의 가용 LWP로부터의 연결 및 해제되도록 사용자 공간 내에서 스케줄링된다. LWP는 프로세스의 쓰레드에서만 이용할 수 있다. 즉, 쓰레드는 이들 LWP 상에서 스케줄링되는 것이다.
대부분의 경우, 쓰레드는 PTHREAD_SCOPE_PROCESS가 된다. 따라서, 쓰레드는 LWP 간을 이동할 수 있으며, 이를 통해 쓰레드 성능이 개선된다(이는 THR_UNBOUND 상태에서 Solaris 쓰레드를 생성하는 것과 동일하다). 쓰레드 라이브러리는 다른 쓰레드 중 해당 커널을 통해 지원받게 되는 쓰레드를 결정하게 된다.

> 시스템 범위(바운드 쓰레드)
바운드 쓰레드는 PTHREAD_SCOPE_SYSTEM으로 생성된다. 바운드 쓰레드는 LWP에 영구적으로 연결된다. 각 바운드 쓰레드는 쓰레드의 수명이 다할 때까지 LWP에 연결된다. 이는 THR_BOUND 상태에서 Solaris 쓰레드를 생성하는 것과 마찬가지이다. 대체 시그널 스택을 제공하기 위해, 또는 Realtime 스케줄링과 함께 특수 스케줄링 속성을 사용하기 위해 쓰레드를 연결할 수 있다. 모든 스케줄링은 운영 환경에 의해 수행된다.

 

> 취소 (cancellation)
쓰레드 취소 기능을 통해 쓰레드는 해당 프로세스에서 여타 다른 쓰레드의 실행을 종료시킬 수 있다. 타깃 쓰레드(취소 대상)는 취소 요청을 보류시키고, 취소 통보 시 실행되는 애플리케이션별 삭제(cleanup) 작업을 수행하게 된다.
pthread 취소 기능은 비동기식 또는 유예식(deferred) 쓰레드 종료가 지원된다. 비동기식 취소는 언제든지 실행될 수 있지만, 유예식 취소는 지정된 시점에서만 실행된다. 유예식 취소가 기본 타입으로 설정된다.

 

> 동기화
동기화는 동시적으로 실행되는 쓰레드를 위해 공유 데이터에 대한 프로그램 플로우 및 액세스를 제어할 수 있도록 지원한다.
상호 배제 락(mutex lock), 읽기 · 쓰기 락(read · write lock), 조건 변수(condition variables), 세마포어(semaphore) 등 4가지 동기화 모델이 제공된다.

 

상호 배제 락(mutex)은 한번에 1개의 쓰레드만 특정 코드 섹션을 실행하거나 특정 데이터에 액세스하도록 허용한다.

읽기 · 쓰기 락은 보호되는 공유 자원에 대해 동시 읽기 및 배타적 쓰기를 허용한다. 자원을 변경하기 위해, 쓰레드는 먼저 배타적 쓰기 락을 확보해야 한다. 배타적 쓰기 락은 모든 읽기 락이 해제되기 전까지는 허용되지 않는다.

 

조건 변수는 특정 조건이 true가 될 때까지 쓰레드를 차단한다.

카운팅 세마포어는 일반적으로 자원에 대한 액세스를 조정한다. 카운트는 세마포어에 액세스할 수 있는 쓰레드 수에 대한 제한 값이다. 카운트에 도달하면 세마포어는 차단된다

애플리케이션 개발자들에게 있어, Solaris 64비트와 32비트 운영 환경 간의 가장 큰 차이점은 사용되는 C-언어 데이터 타입 모델이다. 64비트 데이터 타입은 long 및 포인터가 64비트폭인 LP64 모델을 사용한다. 기타 모든 기본 데이터 타입은 32비트 구현을 그대로 유지하고 있다. 32비트 데이터 타입은 int, long 및 포인터가32비트인 ILP32 모델을 사용한다.

 
다음은 64비트 환경의 주요 특징과 사용 시 고려사항에 대해 간략하게 설명한 것이다.
 
대형 가상 어드레스 공간
 

64비트 환경의 경우, 프로세스는 최고 64비트의 가상 어드레스 공간 또는 18exabyte를 가질 수 있다. 이는 현재 32비트 프로세스의 최대값인 4GB의 40억 배에 해당한다. 그러나, 일부 플랫폼들은 하드웨어 제한으로 인해 풀 64비트 어드레스 공간을 지원하지 못할 수도 있다. 대형 어드레스 공간으로 기본 설정된 스택 크기(32비트의 경우 1MB, 64비트의 경우 2MB)에서 생성할 수 있는 쓰레드 수도 증가하게 된다. 기본 설정된 스택 크기에서 쓰레드 수는 32비트 시스템의 경우 약 2,000개, 64비트 시스템의 경우 약 8조 개에 달한다.

 
커널 메모리 리더
 

커널은 내부적으로 64비트 데이터 구조를 사용하는 LP64 객체이기 때문에 libkvm, /dev/mem 또는 /dev/kmem을 사용하는 기존 32비트 애플리케이션은 제대로 작동할 수 없으며 64비트 프로그램으로 전환해야 한다.

 
/proc 제약 조건
 

/proc을 사용하는 32비트 프로그램은 32비트 프로세스를 볼 수 있지만 64비트 프로세스를 이해할 수는 없다. 따라서, 프로세스를 설명한 기존 인터페이스 및 데이터 구조는 관련 64비트 프로세스를 포함할 수 있는 충분한 크기를 가지고 있지 않다. 이러한 프로그램들은 32비트 및 64비트 프로세스 모두에서 작동할 수 있도록 64비트 프로그램으로 재컴파일 되어야 한다.

 
64비트 라이브러리
 

32비트 애플리케이션은 32비트 라이브러리와 연결되어야 하며, 64비트 애플리케이션은 64비트 라이브러리와 연결되어야 한다. 노후된 라이브러리를 제외한 모든 시스템 라이브러리는 32비트 및 64비트 버전으로 제공된다. 그러나 그 어떤 64비트 라이브러리도 정적인 형태로 제공되지는 않는다.

 
64비트 계산
 

64비트 계산 방식은 과거의 32비트 Solaris 버전에서도 오랫동안 사용되어 왔지만, 64비트 구현은 정수 연산 및 파라미터 전달(parameter passing)을 위해 완전 64비트 머신 레지스터를 제공한다.

 
대용량 파일(Large File)
 

애플리케이션이 대용량 파일 지원 만을 요구하는 경우, 32비트를 그대로 유지하면서 Large File 인터페이스를 사용할 수도 있다. 그러나, 64비트 기능을 완전히 활용하기 위해서는 애플리케이션을 64비트로 변환하는 것이 효과적이다

+ Recent posts