[OS] Thread
쉰 일곱 번째 포스팅
안녕하세요! 쉰 일곱 번째 포스팅으로 찾아뵙게 되어 반갑습니다!♥
오늘의 포스팅 내용은 스레드에 관한 내용입니다.
자세한 내용을 알아보러 갑시다❗️
[Boongranii] Here We Go 🔥
1️⃣ What is thread?
📃 Concept & why
스레드(Thread)는 CPU 이용의 기본 단위로, 프로그램의 실행 흐름을 나타낸다. 각 스레드는 스레드 ID, 프로그램 카운터, 레지스터 세트 및 스택을 포함하는 자체적인 공간을 가지고 있다.
또한, 동일 프로세스에 속한 다른 스레드와 자원을 공유한다. 이런 자원은 코드 섹션, 데이터 섹션 및 다른 운영체제 자원들이다.
👍 Motivation
- 현대의 대부분의 응용 프로그램은 멀티 스레드로 구성되어 있다.
- 응용 프로그램의 여러 작업은 별도의 스레드로 구현될 수 있다.
- 화면 업데이트, 데이터 fetch, 맞춤법 검사 등
- 프로세스 생성은 무거운 작업이지만, 스레드 생성은 가벼운 작업이다.
- 코드를 단순화하고 효율성을 높일 수 있다.
- Kernel은 일반적으로 멀티 스레드로 구성되어 있다.
스레드를 사용하면 여러 작업을 동시에 수행할 수 있으며, 이는 응용 프로그램의 성능을 향상시키고 사용자 경험을 개선 가능하다.
📃 Multithreaded Server Architecture
멀티 스레드 서버 아키텍처는 서버가 여러 스레드를 사용하여 동시에 여러 클라이언트 요청을 처리하는 방식이다.
이 아키텍처는 클라이언트 요청이 많을 때 효율적으로 대응 가능하며, 동시에 여러 작업을 처리할 수 있어서 시스템의 성능을 향상시킬 수 있다.
🎶 주요 특징
- 멀티 스레드
- 각 클라이언트 요청을 처리하기 위해 별도의 스레드가 생성된다. 서버가 여러 클라이언트와 동시 상호 작용이 가능하다.
- 스레드 풀
- 사전에 생성된 스레드 집합을 유지하고, 클라이언트 요청이 들어오면 사용 가능한 스레드 중 하나를 할당하여 처리한다. 이를 통해 스레드 생성 및 삭제의 오버헤드를 줄일 수 있다.
- 비동기 I/O
- 멀티 스레드 서버는 일반적으로 비동기 I/O를 사용하여 입출력 작업을 효율적으로 처리한다. 한 스레드가 입출력 작업이 완료될 때까지 대기하지 않고 다른 작업을 처리할 수 있도록 한다.
📃 Thread's benfits
- Responsiveness (응답성)
- 스레드를 사용하면 프로세스의 일부가 차단된 경우에도 실행이 계속될 수 있다. UI와 같이 사용자와 상호작용하는 부분에서 중요하다.
- Resource Sharing (자원 공유)
- 스레드는 프로세스의 자원을 공유한다. 이는 공유 메모리나 메시지 전달보다 더 쉽고 간단한 방식으로 자원을 공유할 수 있다.
- Economy (경제성)
- 프로세스를 생성하는 것보다 스레드를 생성하는 것이 비용이 덜 든다. 또한, 스레드 전환 비용이 context switching 비용보다 낮기 때문에 스레드를 사용하면 시스템 리소스를 효율적으로 사용 가능하다.
- Scalability (확장성)
- 멀티스레드를 사용하면 프로세스가 다중 프로세서 아키텍처를 활용할 수 있다. 이는 프로세스가 여러 개의 코어 또는 프로세서를 활용하여 작업을 병렬로 처리할 수 있도록 한다.
이러한 이점들을 통해 스레드를 사용하는 것이 유용하다고 할 수 있다.
2️⃣ Multicore Programming
멀티코어 프로그래밍은 멀티코어 또는 멀티 프로세서 시스템에서 프로그램을 개발하는 것을 의미한다. 이는 여러 코어 또는 프로세서가 동시에 수행할 수 있는 시스템에서 프로그램을 작성하는 것을 의미하며, 이는 개발자에게 몇 가지의 챌린지를 제공한다.
- Dividing activities
- 프로그램의 멀티코어 또는 프로세서로 분할하는 것은 복잡할 수 있다. 프로그램의 각 부분을 어떻게 분할하여 여러 코어에 할당할지 결정하는 것이다.
- Balance
- 작업을 균형있게 분산하여 모든 코어가 비슷한 작업 부하를 가지도록 하는 것이 중요하다. 부하가 불균형하면 일부 코어는 과부하가 걸릴 수 있고, 다른 코어는 유휴 상태일 수 있다.
- Data splitting
- 데이터를 멀티코어, 프로세서로 효율적으로 분할하는 것이 중요하다. 데이터가 공유되거나 동기화되어야 하는 경우 충돌을 방지하기 위해 필요하다.
- Data dependency
- 프로그램의 여러 부분이 서로 종속되어 있는 경우, 이를 어떻게 처리할지 결정해야 한다. 데이터 의존성은 멀티코어 환경에서 작업을 병렬화하는 것을 어렵게 할 것이다.
- Testing and debugging
- 멀티코어 프로그램을 테스트하고 디버깅하는 것은 싱글코어 프로그램보다 어려울 수 있다.
멀티코어 프로그래밍은 복잡한 챌린지지만, 성능을 극대화하고 시스템의 리소스를 효율적으로 활용하기 위해 중요하다.
Concurrent execution on single-core system
Parallelism on a multi-core system
📋 Types of parallelism
- Data parallelism
- 데이터 병렬성은 동일한 작업을 수행하는 여러 코어에 동일한 데이터의 하위 집합을 분산시키는 것이다. 각 코어는 자체적으로 데이터 하위 집합을 처리하며 동일한 작업을 반복하여 수행한다. 이러한 방식으로 데이터가 병렬적으로 처리되므로 전체 작업을 빠르게 완료할 수 있다.
- ex. 이미지 처리에서 여러 코어에 이미지의 각 픽셀을 처리하는 작업을 분배하여 데이터 병렬성을 활용 가능하다. 각 코어는 다른 부분을 처리하며, 각 픽셀에 동일한 연산을 적용해 처리 속도를 높인다.
- Task parallelism
- 작업 병렬성은 각각의 작업이 독립적으로 실행될 수 있도록 여러 코어에 스레드를 분산시키는 것이다. 각 스레드는 서로 다른 작업을 수행하며 의존하지 않는다. 이러한 방식으로 여러 작업이 병렬적으로 실행되어 전체적으로 시스템 처리량을 향상 시킨다.
- ex. 웹 서버에서 요청을 처리하는 작업을 분산시키는 것이 예 중 하나이다. 각 요청에 대한 스레드가 생성되어 병렬로 처리되므로, 시스템은 동시에 여러 클라이언트의 요청을 처리할 수 있다.
이 2가지 병렬성은 멀티코어 시스템에서 성능을 극대화하는 데 사용된다.
3️⃣ Multithreading Models
멀티스레드 모델은 사용자 스레드와 커널 스레드 사이의 관계를 설명하는 방식이다. 여기에는 3가지 모델이 존재한다.
- Many-to-One
- One-to-One
- Many-to-Many
📜 Many-to-One
다대일 모델은 여러 개의 사용자 수준 스레드가 하나의 커널 스레드에 매핑되는 방식이다. 이 모델에서는 사용자 스레드는 사용자 스레드 라이브러리에 의해 관리되며, 각각의 사용자 스레드는 커널에 의해 보이지 않는다. 대신, 이러한 스레드들은 사용자 공간에서 직접 관리된다.
- 하나의 커널 스레드에 여러 사용자 스레드 매핑
- 하나의 스레드가 블록되면 모든 스레드가 블록됨
- 한 스레드가 입출력 작업 등으로 블록되면, 해당 커널 스레드가 블록되므로 매핑된 모든 사용자 스레드도 블록된다.
- 다중 스레드가 병렬로 실행되지 않을 수 있음
- 멀티코어 시스템에서는 하나의 커널 스레드만이 동시에 실행될 수 있기 때문에 이 모델에서는 한 번에 하나의 사용자 스레드만이 실행될 수 있다.
- 현재는 많이 사용되지 않는 모델임
- ex. Solaris Green Threads, GNU Portable Threads
다대일 모델은 단순하고 경제적인 장점이 있지만, 성능 및 확장성 문제로 현재는 많이 사용되지 않는다.
📜 One-to-One Model
일대일 모델은 각 사용자 스레드가 하나의 커널 스레드와 일대일로 매핑되는 방식이다. 이 모델에서는 각각의 사용자 스레드가 커널에 의해 별도로 관리되며, 따라서 각 스레드에 대해 별도의 스케줄링 및 관리가 가능하다.
- 각 사용자 스레드가 커널 스레드와 일대일로 매핑
- 사용자 스레드 생성 시 커널 스레드 생성
- 다중 스레드 간 병렬 실행 가능
- 이 모델에서는 각 스레드가 별도의 커널 스레드에 매핑되기 때문에, 멀티코어 시스템에서 여러 스레드가 동시에 병렬로 실행될 수 있다.
- 한 프로세스 내 스레드 수 제한
- 프로세스 내에 생성할 수 있는 스레드가 제한될 수 있다. 커널 스레드 생성 및 관리의 오버헤드로 인해 발생할 수도 있다.
- ex. Windows, Linux, Solaris9
일대일 모델은 매우 유연하고 병렬성을 높일 수 있어 많은 현대 운영체제에서 주로 사용되는 모델 중 하나이다. 각 스레드가 독립적으로 관리되므로 이 모델은 많은 병렬성을 제공 가능하다!
📜 Many-to-Many Model
다대다 모델은 많은 사용자 스레드가 많은 커널 스레드에 매핑되는 방식이다. 이 모델은 각각의 사용자 스레드가 각각의 커널 스레드에 매핑될 수 있으므로, 병렬성과 확장성 면에서 유연성을 제공한다.
- 다수의 사용자 스레드가 다수의 커널 스레드에 매핑
- 운영체제가 충분한 수의 커널 스레드를 생성
- 이 모델은 운영체제가 필요한 만큼의 커널 스레드를 동적으로 생성할 수 있도록 한다. 이는 더 많은 병렬성과 동시성을 제공한다.
- Solaris9 이전 버전 및 Windows ThreadFiber 패키지를 사용하는 경우에 해당
- 이러한 환경에서는 다대다 모델을 사용하여 여러 사용자 스레드를 여러 커널 스레드에 매핑한다.
다대다 모델은 높은 병렬성과 동시성을 달성할 수 있게 한다. 사용자 스레드와 커널 스레드 간의 매핑을 최적화하고, 시스템의 성능을 향상 시킨다.
📜 Two-Level Model
이 모델은 사용자 스레드와 운영체제 스레드 간에 중간 계층을 두는 방식으로 동작한다. 즉, 사용자 스레드는 운영체제 스레드와는 별도로 관리되지만, 사용자 스레드와 운영체제 스레드 간에 매핑이 이루어진다.
이 모델은 유연성과 효율성을 조합한 방식으로 동작한다.
4️⃣ Thread Library
스레드 라이브러리는 개발자가 스레드를 생성하고 관리하기 위한 API를 제공한다. 이러한 라이브러리는 스레드를 다루는 다양한 작업을 간단하게 할 수 있도록 도와준다.
🚃 구현 방식
- 사용자 공간에서 라이브러리 구현
- 이 방식은 모든 스레드 관련 작업이 사용자 공간에서 처리되며, 운영체제의 도움 없이 구현된다. 이 방식은 빠르고 유연하며, 일반적으로 운영체제에 대한 의존성이 낮다. 그러나 사용자 스레드를 관리하기 때문에 운영체제의 기능을 완전히 활용할 수 없는 단점이 존재한다.
- 운영체제가 지원하는 커널 수준 라이브러리
- 이 방식은 운영체제의 지원을 받아 스레드 관리를 구현한다. 스레드의 생성, 스케줄링 및 동기화 등의 작업이 운영체제에 의해 효율적으로 처리된다. 그러나 운영체제에 의존하기 때문에 이식성이 떨어질 수 있다.
🎫 예시
- Pthread
- POSIX(IEEE 1003.1c) 표준에 따라 스레드 생성 및 동기화를 위한 API를 제공한다. 개발자가 스레드 라이브러리의 동작을 명시할 뿐 구현은 라이브러리 개발에 달려있다. 이것은 사용자, 커널 수준 둘 다에서 구현될 수 있다.
- Java Thread
- Java 언어에서 스레드를 관리하기 위한 내장 라이브러리를 제공한다. 이는 JVM에 의해 관리되며, 일반적으로 운영체제의 스레드 모델을 사용하여 구현된다. 이것은 Java 언어의 멀티스레딩을 지원하는 중요한 요소이다.
이러한 스레드 라이브러리들은 개발자가 복잡한 멀티스레드 애플리케이션을 쉽게 구현할 수 있도록 도와준다.
5️⃣ Threading Issues
스레딩 이슈는 멀티스레딩 환경에서 발생하는 다양한 문제들을 의미한다. 발생 원인은 경쟁 상태, 데드락, 교착 상태, 동기화 문제, 스레드 안전성등 과 같은 이유로 발생할 수 있다.
📂 Semantics of system calls
fork()
시스템 콜은 현재 프로세스를 복제하여 새로운 프로세스를 생성한다. fork()
콜이 발생할 때, 현재 프로세스의 모든 스레드가 복제되는 것이 아니라, 호출한 스레드만 복제된다. 근데 또 상황에 따라서 달라질 수 있다.
일부 UNIX 시스템은 fork()
콜에 대한 2가지 버전을 제공한다. 하나는 스레드가 복제되는 것을 지원하고, 다른 하나는 단일스레드만 복제되는 것이다. 따라서 이러한 시스템에서는 스레드를 사용하는 프로그램에서 fork()
콜 시 주의해야 한다.
exec()
호출은 현재 프로세스에 있는 모든 스레드에 영향을 준다. 따라서 exec()
호출 이후에는 이전 프로세스의 상태와 스레드는 모두 사라지고, 새로운 프로그램이 실행된다.
📂 Signal Handling
시그널은 UNIX 시스템에서 특정 이벤트가 발생했음을 프로세스에 알리는 데 사용된다. 시그널 핸들러를 사용하여 시그널을 처리한다.
- 시그널이 발생하는 과정
- 특정 이벤트(사용자가 Ctrl+C를 누르는 행위 등)로 인해 시그널이 생성된다.
- 프로세스에 시그널 전달
- 생성된 시그널은 프로세스에 전달된다.
- 시그널 핸들러 처리
- Default 핸들러: 시그널을 처리하는 기본 동작을 수행하는 핸들러이다. 커널은 시그널을 처리하기 위해 이러한 기본 핸들러를 실행한다.
- User-defined 핸들러: 개발자가 정의한 사용자 정의 동작을 수행하는 핸들러이다. 이 핸들러는 기본 핸들러의 동작을 대체할 수 있다.
단일 스레드 환경에서는 시그널이 프로세스에 전달된다. 하지만, 멀티 스레드 환경에서는 시그널이 어느 스레드에 전달 되어야 하는지에 대한 결정이 필요하다.
🚎 멀티 스레드에서 시그널 전달 위치
- 시그널을 해당하는 스레드에 전달한다.
- 시그널을 프로세스의 모든 스레드에 전달한다.
- 시그널을 프로세스의 특정 스레드에 전달한다.
- 프로세스의 모든 시그널을 처리하기 위해 특정 스레드를 지정한다.
이러한 방식으로 멀티 스레드 환경에서 시그널을 처리하면 프로세스의 동작을 더욱 정확하게 제어 가능하다.
📂 Thread Cancellation
스레드 취소는 스레드가 완료되기 전에 취소하는 것을 의미한다. 이는 종종 스레드의 실행을 중지 시키거나 리소스를 정리할 필요가 있는 경우에 사용된다.
스레드 취소에는 2가지 일반적인 접근 방식이 존재한다.
- Asynchronous cancellation
- 비동기적 취소는 대상 스레드를 즉시 종료시킨다. 대상 스레드가 어디에서든 중지될 수 있으므로 실행 중인 작업에 영향을 줄 수 있다.
- Deferred cancellation
- 지연된 취소는 대상 스레드가 주기적으로 취소 여부를 확인하도록 허용한다. 취소 확인 지점에 도달할 때까지 대상 스레드가 계속 실행된다. 주기적으로 취소를 확인하므로 실행 중인 작업이 완료될 때까지 대상 스레드를 중지하지 않는다.
일반적으로 Pthread와 같은 스레드 라이브러리에는 아래와 같은 방법으로 스레드를 생성하고 취소한다.
1
2
3
4
5
6
7
8
9
pthread_t tid;
// create the thread
pthread_create(&tid, 0, worker, NULL);
...
// cancel the thread
pthread_cancel(tid);
- Cancellation request invocation
- 스레드 취소 요청을 호출하면, 실제 취소는 스레드의 상태에 따라 달라진다.
- 스레드 취소 요청을 호출하면, 실제 취소는 스레드의 상태에 따라 달라진다.
- Cancellation disabled state
- 스레드가 취소를 비활성화한 경우, 취소는 스레드가 다시 활성화될 때까지 대기 상태로 남아 있는다.
- Default type is deferred
- 기본적으로 지연된 취소 유형이 적용된다. 이 경우, 취소는 스레드가 취소 확인 지점에 도달했을 때만 발생하며, 이후 cleanup handler가 호출된다.
- Thread cancellation on Linux systems
- 리눅스 시스템에서는 시그널을 사용하여 스레드 취소를 처리한다.
📂 Thread-Local Storage
TLS는 각 스레드가 데이터의 복사본을 가질 수 있도록 하는 메커니즘이다.
- 스레드별 데이터 복사
- TLS를 사용하면 각 스레드는 자신만의 데이터 복사본을 가질 수 있다. 이는 다른 스레드의 영향을 받지 않고 스레드 간에 데이터를 공유할 수 있는 방법을 제공한다.
- 스레드 풀을 사용할 때 유용함
- 스레드 풀과 같은 상황에서는 스레드 생성 프로세스를 제어하지 못할 수 있다. TLS를 사용하면 스레드 간에 안전하게 데이터를 공유할 수 있다.
- 로컬 변수와의 차이
- 로컬 변수는 함수 호출 중에만 유효한 반면, TLS는 여러 함수 호출 사이에서도 유지된다. 따라서 TLS는 함수 간에 데이터를 공유하는 데 사용될 수 있다.
- 정적 데이터와의 유사점
- TLS는 정적 데이터와 유사한 면이 있지만, TLS는 각 스레드에 대해 고유하다. 정적 데이터는 모든 스레드에서 공유되는 반면, TLS는 각 스레드에서만 볼 수 있다.
TLS는 멀티 스레드 환경에서 데이터를 효율적으로 관리하기 위한 중요한 메커니즘이다.
📂 Scheduler Activations
Many-to-Many와 Two-level 모델은 애플리케이션에 할당된 적절한 수의 커널 스레드를 유지하기 위해 통신이 필요하다. 일반적으로 사용자 스레드와 커널 스레드 사이에 중간 데이터 구조인 Lightweight Process: LWP
를 사용한다.
- LWP
- LWP는 사용자 스레드와 커널 스레드 간의 중간 계층으로 작동한다. LWP는 가상 프로세서처럼 동작하여 프로세스가 사용자 스레드를 실행할 수 있도록 한다. 각 LWP는 커널 스레드에 연결되어 있다.
- LWP 생성
- 얼마나 많은 LWP를 생성해야 하는지는 시스템 및 애플리케이션의 요구에 따라 달라진다. 일반적으로 애플리케이션이 사용할 수 있는 프로세서 수, 스레드의 수, 작업 부하 등을 고려하여 결정된다.
- Scheduler activations
- 스케줄러 활성화는 커널에서 스레드 라이브러리의 업콜 핸들러로의 통신을 제공한다. 이 통신을 통해 애플리케이션은 올바른 수의 커널 스레드를 유지할 수 있다. 즉, 커널은 애플리케이션에 적절한 수의 LWP를 생성하고 유지하기 위해 스레드 라이브러리로 요청을 보낼 수 있다.
이러한 메커니즘들은 멀티스레드 애플리케이션에서 효율적인 스레드 관리를 가능하게 하며, 시스템 리소스를 최적으로 활용할 수 있도록 한다.
6️⃣ 마무리
오늘의 내용을 간략하게 정리해보도록 하겠다.
멀티스레딩은 현대 소프트웨어 개발에서 중요한 역할을 맡고 있다. 이는 CPU 활용률을 최대화하고 응답성을 향상 시키는 데에 핵심적인 역할을 한다.
스레드는 프로세스 내에서 실행되는 독립적인 실행 흐름으로, 각각이 별도의 작업을 수행 가능하다. 이를 통해 멀티태스킹과 동시성을 지원하며, 다양한 작업들을 병렬로 처리할 수 있다.
그러나 멀티스레딩은 개발자에게 다양한 챌린지를 제시한다. 예를 들어서 스레드 간의 동기화 문제, 데이터 공유 문제와 같은 문제를 말이다. 또한, 멀티프로세서 시스템에서는 병렬성과 동시성을 효율적으로 관리해야 한다.
이러한 챌린지를 극복하기 위해서는 스레드 생성 및 관리, 스레드 간의 통신과 동기화 메커니즘 등에 대한 이해가 필요하다. 또한, 시스템 특성에 맞도록 적절한 스레드 모델과 동작 방식을 택해야 한다.
긴 글 읽어 주셔서 감사합니다😌
댓글남기기