이번에는 쓰레드에 대해서 공부한 내용을 정리해 보려고 한다.
지난 포스팅에서 프로세스에 대해서 정리를 한 적이 있었다. 프로세스는 스케줄링의 단위로서 실행 단위(Execution unit)이다. 또한 소유하고 있는 자원에 대한 보호(Protection domain) 개념을 가지고 있기도 하다. 지금까지는 하나의 실행 흐름을 가지고 실행중인 프로그램에 대해서만 다루었기 때문에 프로세스만 가지고 설명이 가능했다. 하지만 프로세스의 처리 속도가 점점 빨라져야 할 필요성에 맞추어, 하나의 프로세스가 수행해야 할 여러 작업들을 나누어 수행할 수 있는 설계가 필요해졌고, 이에 생겨난 개념이 쓰레드(Thread)이다.
쓰레드는 프로세스 내의 실행 흐름이다. 이 역시 실행 단위(Execution unit)으로 볼 수 있다. 다만, 쓰레드는 프로세스보다 작은 단위(fiber grain)이며, 프로세스와 같은 Protection Domain은 없고 한 프로세스 안에 있는 모든 쓰레드는 상태를 공유한다. 프로세스와 쓰레드에 여러가지 차이점이 있는데 먼저 프로세스는 서로 직접 접근할 수 없지만(커널을 사용하면 가능), 쓰레드는 서로 직접 접근하는 것이 가능하다. 또한 프로세스는 통신을 위해서 IPC가 필요하나, 스레드는 별도의 통신이 필요하지 않다.
쓰레드의 가장 큰 장점은 프로세스에서 할 작업을 여러개로 쪼개어 병렬적으로 작업을 완수할 수 있다는 점이다. 이는 Cooperative Process(협력적 프로세스)와 차이가 있는데, Cooperative Process는 프로세스간 통신이 필요하고 문맥 교환 비용도 발생하기 때문에 비용이 많이 든다는 한계가 있다. 그래서 프로세스 내에서 협력을 할 수 있는 쓰레드를 만든다면, 프로세스보다 적은 비용을 가지고 협력적 프로세스가 하는 일을 동일하게 수행할 수가 있다. 이와 같은 이유로 쓰레드를 "LightWeight Process(LWP)"로 부르기도 한다.
위의 그래프는 쓰레드의 수와 Throughput의 관계를 나타내었다. 여기에서 쓰레드 수가 증가함에 따라 초반에는 Throughput이 비례해서 증가함을 알 수 있다. 그 이유는 쓰레드 숫자가 커질수록, CPU의 Utilization이 증가하기 때문이다. 반면 일정 수준 이상이 되면 오히려 Throughput이 감소하는 것을 알 수 있는데, 임계값을 넘기게 되면 thread switching 비용이 증가하기 때문이다. CPU 수가 많은 시스템일수록 쓰레드를 이용하는 것이 유리하다.
싱글 쓰레드와 멀티 쓰레드를 비교한 그림이다. 보이는 것 처럼 Code, Data, File은 멀티쓰레드에서 같이 사용하고 있고, register와 stack만 쓰레드 별로 다르게 사용하고 있음을 알 수 있다. 요즘에 사용하는 대부분의 프로그램은 멀티 쓰레드 방식을 사용한다.
웹 브라우저를 예로 들면, 하나의 화면에서 이미지나 텍스트도 불러와야 하고 또 네트워크에서 데이터도 가져와야 한다. 그리고 여러명의 사용자가 한 번에 접속하는 경우도 발생한다. 이러한 경우에 만약 싱글스레드를 사용하면 한 번에 하나의 작업만 처리하거나, 한 번에 한 명의 클라이언트만 상대할 수 있기 때문에 성능면에서 매우 불리하다.
최근에는 멀티 프로세서보다 멀티 코어 방식을 선호한다. 그 이유는 멀티 프로세서에서 CPU가 늘어날수록 BUS도 늘어나게 되는데, BUS가 늘어남에 따라 캐쉬 데이터의 일관성을 유지해주는 Cache Consistency가 exponential하게 복잡해 지기 때문이다. 이 문제는 멀티코어에서 캐쉬를 공유함으로서 해결할 수가 있다.
쓰레드는 지원하는 주체에 따라서 User Thread와 Kernel Thread로 나뉜다. 유저 쓰레드는 커널 영역 위에서 지원되며, 일반적으로 user level의 라이브러리를 통해 구현된다. 라이브러리에서 쓰레드를 생성하고 스케줄링과 관련된 관리를 해준다. 유저 쓰레드의 장점은 동일한 메모리 영역에서 Thread가 생성, 관리되므로 이에 대한 속도가 빠르다는 점이다. 반면에 단점은 여러개의 유저 쓰레드 중 하나의 쓰레드가 시스템 콜 요청으로 블락되면 나머지 모든 쓰레드 역시 블락된다는 점이다. 이는 커널이 여러 개의 유저 쓰레드를 하나의 프로세스로 간주하기 때문이다.
커널쓰레드의 경우는 조금 다르다. OS에서 쓰레드를 지원한다. 커널 영역에서 쓰레드의 생성, 스케줄링 등을 관리하며 User Level보다 우선순위가 높은 클래스에 들어간다. 커널 쓰레드의 장점은 쓰레드가 시스템 콜을 호출하여 블락되면, 커널은 다른 쓰레드를 실행함으로서 전체적인 thread blocking이 발생하지 않는다. 또한 멀티 프로세서 환경에서 커널이 여러 개의 쓰레드를 다른 프로세스에 할당할 수 있다는 점이다. 반면 커널 쓰레드의 단점은 유저 쓰레드보다 생성 및 관리가 느리다.
우리는 멀티쓰레드 환경을 구축하기 위해서 방금 살펴본 유저 쓰레드와 커널 쓰레드를 맵핑하는 작업을 거쳐야 한다. 여기에 크게 세 가지가 있는데 하나씩 살펴보도록 한다
Many-to-one
Many-to-one 방식은 쓰레드 관리가 유저 레벨에서 이루어지며, 여러 개의 유저 쓰레드가 하나의 커널 쓰레드로 매핑이 된다. 여기에서 추가적으로 설명을 하면, 멀티 쓰레드 프로그램이 실행이 될 때 thread library에서 여러개의 쓰레드를 스케줄링 하고, 사용자로 하여금 하나의 프로그램이 여러 개의 쓰레드가 동작하게 하고 있는 착각을 하게 만든다. 이 방식은 커널 쓰레드를 지원하지 못하는 시스템에서 사용된다.
한계점이 있다면 한 번에 하나의 쓰레드만 커널에 접근할 수 있어서 하나의 쓰레드가 커널에 접근(시스템 콜)하면 나머지 쓰레드들은 대기해야 한다. 그러므로 진정한 concurrency는 지원하지 못한다. 또한 커널의 관점에서 보았을 때, 여러개의 쓰레드는 하나의 프로세스이기 때문에 멀티 프로세서이더라도 여러개의 프로세서에서 동시에 수행할 수는 없다.
One-to-one
One-to-one 방식은 각각의 유저 쓰레드를 커널 쓰레드로 맵핑하는 방식이다. 유저 쓰레드가 생성되면, 그에 따른 커널 쓰레드가 생성된다. 이 방식은 Many-to-one에서 한계로 지적되었던, 시스템 콜 호출 시 다른 쓰레드들이 블락되는 문제를 해결할 수 있다. 또한 여러개의 쓰레드를 멀티 프로세서에서 동시적으로 수행할 수도 있다. 이 방식의 한계점은 커널 쓰레드도 한정된 자원이기에 무한정으로 생성할 수가 없다는 점, 그리고 쓰레드를 생성하거나 사용할 때 그 갯수에 대한 고려가 필요하다는 점이다.
Many-to-many
Many-to-many 방식은 여러개의 유저 쓰레드를 여러 개의 커널 쓰레드로 매핑하는 방식이다. 이 방식은 Many-to-one과 One-to-one의 문제점을 해결한다. 커널 쓰레드는 생성된 유저 쓰레드와 같거나 적은 수만큼 생성이 되어 적절히 스케줄링을 할 수 있다. One-to-one 처럼 사용할 쓰레드 수에 대해 고민할 필요가 없으며, Many-to-one 처럼 쓰레드가 시스템 콜을 사용하는 경우, 다른 쓰레드들이 블락되는 현상에 대해 걱정할 필요가 없다. 커널은 적절히 유저 쓰레드와 커널 쓰레드 매핑을 조절하여 위와 같은 장점을 보장할 수가 있다.
쓰레드 작업을 하다 보면 이전에 없던 이슈가 생기는 경우가 발생한다. 각각의 주제에 대한 이슈를 간단하게 정리해 보고 마무리 하려고 한다.
생성(Creation)
멀티 쓰레드 프로그래밍에서 fork와 exec의 의미는 달라져야 한다. fork의 경우 모든 thread를 가지고 있는 프로세스를 만들 것인지, 아니면 fork를 요청한 thread만을 복사한 프로세스를 만들 것인지에 대한 문제가 남아 있다. 리눅스에서는 2가지 버전의 fork를 만들어 각각의 경우를 처리하도록 하고 있다.
exec의 경우 fork로 모든 thread를 복사했을 때 exec를 수행하면 모든 thread가 새로운 program으로 교체가 된다. 이 때는 fork를 요청한 thread만 복사되는 것이 바람직한 경우가 많다. 하지만 때로는 모든 thread의 복사가 필요할 때도 있다.
삭제(Cancellation)
쓰레드 삭제는 쓰레드의 작업이 끝나기 전에 외부에서 작업을 중지시키는 것을 말한다. 하나의 쓰레드에서 중지 명령은 결국 다른 쓰레드의 모든 작업을 중지시켜야 하는 이슈가 발생한다. 또한 자원이 쓰레드에 할당된 경우 삭제 문제 역시 이슈가 된다. 예를 들어, 시스템의 자원을 사용하고 있는 쓰레드가 중지될 경우 할당된 자원을 함부로 해제할 수 없다. 왜냐하면 다른 쓰레드가 그 자원을 사용하고 있을 지도 모르기 때문이다.
쓰레드 풀(Thread Pool)
쓰레드가 자주 생성되고 제거되는 상황에서 새로 쓰레드를 만드는 시간이 실제 쓰레드가 동작하는 시간보다 긴 경우가 존재한다. 쓰레드는 시스템의 자원이기 때문에, 시스템의 동작을 보장하는 최대한의 쓰레드 수에 대한 제한이 필요하다. 이에 대한 해결 방안으로 나온 것이 쓰레드 풀이다.
쓰레드 풀을 만들어서 프로세스가 새로 실행될 때, 정해진 수 만큼 쓰레드를 만들고 풀에 할당한다. 새로운 쓰레드가 필요하면 풀에서 가져오고, 작업이 끝나면 그 쓰레드를 제거하지 않고 다시 풀에 넣어 둔다. 이와 같이 풀을 통해 제한된 수의 쓰레드를 관리하면, 그러지 않은 경우보다 쓰레드 생성에 걸리는 시간이 줄어든다.
스레드간 IPC
멀티 쓰레드 프로그래밍에서는 쓰레드 간 통신이 필요하다. 쓰레드 간 IPC의 구현은 공유 메모리가 효율적이다. 왜냐하면 쓰레드들은 같은 프로세스의 data 영역을 공유하므로, 자연스러운 공유 메모리가 가능해지기 때문이다. 결국 쓰레드 간의 자원 공유로 인하여 IPC가 최소화 된다.
참고자료
1. 고려대학교 유혁 교수님 운영체제(COSE341) 수업 자료
2. <Operating System Concepts> 9th ed. by A. Silberschatz
'Computer Sci. > Operating System' 카테고리의 다른 글
OS #7. 동기화(Synchronization) Part2 (0) | 2020.04.11 |
---|---|
OS #6. 동기화(Synchronization) Part1 (1) | 2020.04.01 |
OS #4. CPU 스케줄링 (CPU Scheduling) (0) | 2020.03.20 |
OS #3. 프로세스 (Process) (0) | 2019.10.17 |
OS #2. 운영체제 디자인, 커널 관점 설계 (OS Design, Kernel Arch.) (0) | 2019.10.11 |