0. Intro
Hardware Thread, OS Thread , Native Thread , Kernel Thread , User Thread , 그린 Thread 의 개념을 알아보자
우리가 작성한 프로그램은 컴퓨터상에서 아래와 같은 형태로 동작을 합니다. 그래서 기본적으로 컴퓨터를 구성하는 하드웨어가 있고, 하드웨어 및 전체 컴퓨터 시스템을 관리하는 OS가 있습니다. 그리고, 프로그래머가 개발하는 프로그램은 OS를 통해서 하드웨어를 사용하게 됩니다.
총 3개의 레벨로 구성이 되어있는데, 각 쓰레드들은 각 레벨과 관련된 쓰레드들입니다.
1. Hardware Thread
지금까지 우리가 배워왔단 쓰레드 개념은 잠시 잊고 백지상태라고 생각해보자.
1.1 코어(core)의 고민 : 메모리에서 데이터를 기다리는 시간이 꽤 오래 걸린다.
코어에서 프로그램이 실행될 때 그 프로그램은 각종 컴퓨팅 작업 (연산 작업, 연산 작업을 위해 메모리에서 데이터를 읽어오거나, 연산의 결과를 메모리에 쓰는 작업 등) 메모리에 접근하는 작업이 있다.
그런데, 코어에서 실행되는 연산 작업에 비해서 메모리에서 데이터를 기다리는 시간이 상대적으로 오래 걸린다. 그렇게 메모리에서 데이터를 기다리는 동안 코어가 아무일을 하지 않는다면, 어떻게 보면 코어를 낭비하게 되는것이다.
이를 해결하기 위해서 메모리를 기다리는 동안 다른 스레드를 실행하는 건 어떨까 라는 아이디어가 나옵니다.
코어가 하나 있다고 보겠습니다. 코어에서 우리가 작성한 프로그램을 실행해보면, 기본적으로 어떤 연산작업을 하다가, 연산작업을 위해서 메모리에서 데이터를 읽어오든, 혹은 연산 작업한 결과를 메모리에 쓰든 이제 메모리에 접근을 해서 데이터 처리를 해야하는데요. 이 메모리에 접근하는 동안에 이 코어가 아무 일도 하지 않게 된다는 것(Idle Time)입니다. 그러다가 또 다시 컴퓨팅 작업을 하게 되면 코어가 열심히 일을 하겠지만, 또 메모리와 관련해 어떤 작업을 하는 동안에는 코어는 대기하며 쉬게 되는거죠.
이처럼 [메모리에 접근할 때마다 코어가 쉬면 코어를 낭비하는 것이 아니냐] 라는 문제 제기가 있었고, 이를 해결하기 위해서 [메모리에 접근하는 동안 CPU가 독립적으로 또다른 무언가를 실행하는 것은 어떠냐] 라는 아이디어가 나오고, 앞서 실행한 것과는 별개로 독립적으로 다른 것을 실행하는 거죠. 한쪽이 메모리에 접근하는 동안에, 다른 한쪽이 그 코어를 사용하게 되는 그런 개념인겁니다. 그래서 이제는 메모리에 접근하는 그 시간마다 이제 또 다른 뭔가를 실행하게 된거죠. 그러면 이렇게 또 다르게 실행된 얘는 또 중간 중간에 메모리 접근을 하게 되겠죠.
코어는 하납니다. 지금 코어는 하나지만 코어의 사용률을 극대화시키기 위해서 두 개의 서로 다른 하드웨어 스레드를 실행하는거죠.
Hardware thread : OS 관점에서는 가상의 코어
만약에 싱글 코어 CPU에서 하드웨어 쓰레드가 두 개라면 OS는 이 CPU를 듀얼 코어로 인식하고, 듀얼 코어에 맞춰서 OS레벨의 쓰레드들을 스케줄링한다.
물리적인 코어는 하나가 있지만, 하드웨어 스레드가 2개가 있는거에요. 그러면 이제 얘를 바라보는 OS는 얘를 하드웨어 하드웨어 하나하나를 모두 코어로 인식합니다. 그래서 OS입장에서는 이 CPU를 듀얼 코어로 인식하고 듀얼코에 맞춰서 쓰레드들을 스케줄링합니다.
하드웨어 쓰레드들은 우리가 일반적으로 배워왔던 쓰레드랑은 개념이 다른, CPU상에서 혹은 코어 하나하나마다 사용률을 극대화시키기 위해서 개발된 CPU레벨 혹은 하드웨어 레벨 스레드인거죠.
2. OS Thread
이제 OS쓰레드가 뭔지 살펴보겠습니다. OS 스레드는 OS Kernel 영역에 해당되는 부분인데요. 지금까지 일반적으로 배워 알고있던 스레드 개념이 OS 스레드를 의미합니다.
그러면 이 OS 스레드를 이해하기 위해서는 커널의 개념부터 알아봅시다.
2.1 커널(kernel)
- 운영체제의 핵심
- 시스템의 전반을 관리/감독하는 역할
- 하드웨어와 관련된 작업을 직접 수행
2.2 OS 스레드
OS 커널 레벨에서 생성되고 관리되는 스레드로, CPU에서 실제로 실행되는 단위이며, CPU 스케줄링의 단위입니다.
2.2.1 컨텍스트 스위칭
OS 스레드의 컨텍스트 스위칭은 커널이 개입해서 주도적으로 진행합니다. 즉, 컨텍스트 스위칭이 일어날 때마다 유저 모드에서 커널 모드로 전환이 되고 여러 커널 코드가 CPU에서 실행이 되기 때문에 CPU에 리소스를 사용하게 되고, 그래서 스위칭이 끝이나게되면 다시 커널 모드에서 유저모드로 전환되는 과정이기 때문에, 이런 과정에서 비용이 발생을 하는거죠. 시간적인 비용, cpu 리소스를 사용하는 비용 등이 발생합니다.
그말은 사용자 코드와 커널 코드 모두 OS 스레드에서 실행된다는 뜻입니다.
2.2.2 OS 쓰레드는 사용자코드, 그리고 커널코드 또한 실행한다.
중요한 것은 OS 쓰레드에서 개발자가 작성한 사용자 코드가 실행되기도 하고, 또 커널 코드 또한 OS 쓰레드에 대해서 실행이 됩니다.
예를들면, 개발자가 작성한 코드가 OS 쓰레드에서 실행이 되는 중간에 시스템 콜을 사용하면, 이제 이 시스템 콜 때문에 커널 모드로 전환이 되면서 커널 모드에서 커널 코드가 실행이 되요. 그런데 이 커널 코드를 OS 쓰레드에서 실행된다는 겁니다. 그래서 커널 코드 실행이 끝나고 이제 시스템 콜이 완료가 되서 다시 유저 모드로 돌아오면 이어서 우리가 작성한 코드가 실행이 되는거죠. 그래서 이런 의미에서 개발자가 작성한 사용자 코드와 중간중간에 시스템 콜을 사용하게 되면서 실행되는 커널 코드 또한 이 OS 쓰레드에서 같이 실행이 된다. 이렇게 이해하시면 될것같아요.
OS 쓰레드는 아래와 같이 불리기도 함
네이티브(native) 스레드
커널 스레드*
커널-레벨 스레드
OS 레벨 스레드
자 그러면 이 OS 스레드는 아래와 같이 불리기도 합니다.
보통 네이티브라고 하면 운영체제를 말하기 때문에 이 네이티브 쓰레드가 운영체제에서 관리되는 쓰레드라는 의미로 이 네이티브 쓰레드가 사용된다고 보시면 될것 같구요. 나머지는 다 쉬운데 지금 커널 스레드에 패스트리스크를 붙여놯죠. 이게 커널 쓰레드가 맥락에 따라서는 조금 다른 의미로 사용될 수 있기 때문에 그래요. 다른 의미로 사용되는 커널 쓰레드는 말미에 다시 설명하겠습니다.
Quiz 2
OS 쓰레드 여덟 개가 하이퍼 쓰레딩이 적용된 인텔 듀얼코어 위에서 동작한다면, OS 쓰레드들을 어떻게 코어에 균등하게 할당할 수 있을까요?
인텔 듀얼코어이기 때문에 물리적인 코어는 2개입니다. 그런데 하이퍼 쓰레딩이 적용됬기 때문에 하드웨어 쓰레드가 2개씩 있을꺼에요. 그럼 코어마다 2개씩 있으니까 하드웨어 쓰레드는 총 4개씩 있을거에요. 그러면 OS입장에서는 이 하드웨어 쓰레드 각각이 코어로 인식되기 때문에 실제로 이 OS는 코어 4개짜리 CPU로 인식을 하게 됩니다. 그럼 이제 OS 쓰레드가 총 8개라고 했으니까, 이 여덟 개의 OS 쓰레드를 코어 4개에 각각 할당을 하게 되죠.
3. User Thread
대망의 User Thread에 대해 살펴보도록 합시다.
유저 스레드는 유저 프로그램 부분과 관련된 쓰레드입니다. 유저 스레드는 유저-레벨 스레드라고 불리기도 합니다.
유저 쓰레드
쓰레드 개념을 프로그래밍 레벨에서 추상화 한 것
유저 쓰레드는 일단 이렇게 이해하면 쉽습니다.
JAVA에서 쓰레드를 만들어서 실행시키는 코드에요. JAVA는 쓰레드 객체를 하나 만들고, 실행시킴으로써 이 쓰레드를 동작하게 됩니다. JAVA에서는 new Thread라는 개념이 유저 쓰레드 혹은 유저 레벨 쓰레드라고 이해할 수 있는거죠.
그러면 실제로 얘는 어떻게 동작을 하는지 간단하게 설명할건데요. 이 자바에서 제공하는 Thread라는 class의 소스 코드를 보면, start라는 method가 있습니다. start라는 method의 구현 부분을 보면 내부적으로 스타트 제로라는 메서드를 호출해요. 근데 이 스타트 제로는 JNI라는 기술을 통해서 그 바로 아래쪽 레벨에 있는 OS의 시스템 콜을 호출하게 됩니다. 그래서 만약에 그 운영체제가 리눅스라면 클론이라는 시스템 콜을 호출하게 되는거구요. 이 클론이 호출되는 순간 이 리눅스에서는 OS 레벨의 스레드를 하나 생성하게 되는거죠. 그래서 이 OS 레벨의 쓰레드가 하나 생성되면 지금 유저 레벨의 스레드, JAVA로 치면 new Thread()와 연결이 되는 것입니다.
[정리]
유저 스레드가 CPU에서 실행되려면 OS Thread와 반드시 연결되어야 한다.
앞에서 유저 스레드가 쓰레드 개념을 프로그램 레벨에서 추상화 시킨 것이라고 했잖아요.
그러면 이 유저 쓰레드가 CPU에서 실행이 되려면 반드시 OS 쓰레드와 연결이 되어야 합니다.
아래쪽, H/W에 있는 CPU에서 실행되는 쓰레드는 OS Thread가 실행되는 것이고, 실제로 CPU에서 실행되기 위해서는 이 유저 쓰레드, 유저 프로그래밍 레벨에 있는 이 유저 스레드는 반드시 이 OS 스레드를 통해서 CPU에서 실행이 되어야 하고, 그 말은 유저 스레드는 OS스레드와 반드시 연결이 되어야 한다는 것입니다.
4. User thread와 OS thread를 어떻게 연결할 것인가?
총 3가지 연결 방식이 있는데요.
One-to-One model
이거는 오늘날의 자바가 예인데요. New Thread()를 통해 쓰레드를 만들고, 그 쓰레드 객체를 시작하는 순간 최종적으로는 시스템 콜을 통해서 OS레벨의 쓰레드를 만들게되죠. 이처럼 오늘날의 자바는 이 유저 쓰레드와 이 OS 레벨의 쓰레드가 일대일로 연결이 되는 모델입니다.
One-toOne 모델의 특징으로, 쓰레드 관리를 OS에 위임을 하게 되요. 유저 쓰레드와 OS 쓰레드가 1대1로 매핑이 되기 때문에, 스케줄링을 포함해서 쓰레드 관리를 OS에 위임합니다. 그래서 이 스케줄링도 커널이 수행하게 되는거죠.
CPU가 멀티 코어를 가진다고 해도 멀티 코어에 OS 쓰레드를 잘 배분시켜서 동작시킬거고, 1:1로 매핑이 되는 유저 쓰레드와 멀티 코어 환경을 잘 활용하게 된다는 거죠. 그래서 이 모델은 멀티코어도 잘 활용해요.
얘네들은 1:1 매핑관계자나요. 그러면 한 프로세스가 여러개의 쓰레드를 가질 수 있다고 했으니까, 지금 이 모든게 같은 프로세스에 속한 쓰레드라고 해볼게요. 그러면 여기서 만약에 유저 쓰레드가 Block IO를 실행했다고 해보면, 그러면 자연스럽게 이 OS레벨의 이 쓰레드에 대해서 Block IO를 실행하게 될거고, 그러면 이 쓰레드는 블라킹에 대해서 기다리게 되지만, 나머지 쓰레드들은 1:1 매핑이니까 이 둘의 쓰레드는 상관없이 잘 동작을 하게 되겠죠. 그래서 한 쓰레드가 Block이 되어도 다른 쓰레드는 잘 동작합니다. 하지만 이 모델은 지금 OS 레벨의 쓰레드들과 1:1 매핑관계이기 때문에 어떻게 실행되는지에 따라서 레이스 컨디션이 발생할 수 있습니다. 자 이게 원투원 모델이고, JAVA도 one-to-one 모델이라고 했어요.
Many-to-One model
유저 레벨 쓰레드가 여러개가 있고, OS 레벨 쓰레드는 딱 하나만 있는겁니다. 그래서 이 여러 유저 쓰레드가 하나의 OS 쓰레드에 연결이 되는거죠. 이런 구조를 매니 투 원 모델이라 합니다. 얘는 어떤 특징을 지니게 되냐면, 컨텍스트 스위칭이 더 빠릅니다. 왜냐하면, 컨텍스트 스위칭이 유저레벨에서만 일어나기 때문에 커널이 전혀 개입을 하지 않게 되고, 커널이 개입하지 않는다는 말은 컨텍스트 스위칭이 훨씬 더 빨리 끝날 수 있다는 말이거든요. 그래서 매니투원 모델에서는 유저 쓰레드간의 스위칭이 앞에 봤던 one-to-one 모델에 비에서 빠릅니다. 그리고 또 어떤 특징이 있냐면, 지금 OS 쓰레드가 하나밖에 없잖아요. 실제로 이 CPU에서 실행되는 것은 지금 보시는 이 OS 쓰레드라고 했는데 이 OS 쓰레드만 놓고 보면 싱글 쓰레드기 때문에 OS 레벨에서 레이스 컨디션이 일어날 가능성은 뭐 거의 없다고 봐도 될거같구요. Race condition이 일어난다면 유저레벨에서 유저 쓰레드들이 컨텍스트 스위칭 하다가 일어날 건데 상대적으로 one-to-one모델에 비에서 더 적어집니다. OS 쓰레드가 하나이기 때문에 race condition 가능성이 적지만, 멀티 코어를 활용을 못합니다. 실제로 코어에서 실행되는 것은 OS레벨의 쓰레드기 때문에 지금 이 모델에서는 OS 쓰레드가 하나라서 멀티 코어를 활용하지 못하는거죠. 또 어떤 단점이 있냐면, 예를 들어서 이 첫번째 유저 쓰레드가 블락 IO를 호출했다고 해볼께요. 그럼 이 OS 쓰레드에서 Block IO를 호출하게 되는거거든요. 그러면 이 OS 쓰레드가 하나밖에 없기 때문에 이 OS 쓰레드가 Block이 되는 순간 이 전체 유저 쓰레드들도 블락이 되는 거죠. 그래서 한 쓰레드가 block이 되면 모든 쓰레드들이 Block이 됩니다. 이런 단점이 있어요. 그래서 사실 이 문제를 해결하기 위해서 Nonblock IO를 사용하게 됩니다.
Many-to-Many model
끝으로 Many-to-Many model이 있는데요. 이는 앞에서 봤던 one-to-one 모델과 Many-to-one 모델의 장점을 합쳐서 만든 모델이라고 이해하시면 될 것 같습니다. 얘같은 경우에는 유저 쓰레드들이 있구요, 또 적절한 개수의 쓰레드들이 있습니다. 그럼 이 유저 쓰레드들이 OS 쓰레드 위에서 서로 스케줄링되면서 이렇게 실행이 되는 거에요. 이런 형태를 매니 투 매니 모델이라고 하고, 앞에서 살펴보았던 두 가지의 장점을 합친거라고 했기 때문에. 가령 유저 쓰레드간의 스위칭이 빠르면서도, 멀티 코어를 활용하게 되고 하나가 블락이 되어도 전체 유저 쓰레드들은 Block이 되지 않죠. 다른 OS 쓰레드들이 있으니까요. 그 대신 구현이 어렵다는 단점이 있긴 합니다. Go 언어가 이런 모델을 지원한다고 합니다.
Java 초창기 버전은 Many-to-One 쓰레딩 모델을 사용, 이 때 유저 쓰레드들을 그린 쓰레드라고 호칭.
그린 쓰레드라는 용어를 기술문서에서 만나게 되면, OS와는 독립적으로 유저 레벨에서 스케줄링 되는 쓰레드 라는 의미이기도 해요. 그래서 맥락에 따라서 유저 쓰레드와 그린 쓰레드는 동일하기도 해요.
5. Kernel Thread*
조금 다른 맥락에서 Kernel thread를 살펴볼게요.
앞에서는 커널 쓰레드의 의미가 OS 쓰레드와 같은 의미라고 설명했었지만, 어떤 맥락에서는 커널 쓰레드가 다른 의미로 사용되기도 합니다.
Kernel thread OS 커널의 역할을 수행하는 스레드
OS 커널은 OS의 주요 핵심이 되는 역할을 담당하여, 시스템의 전반을 관리감독한다고 했는데, 이때 이 역할을 결국은 코드로 수행을 하게 될거잖아요. 그럼 그 커널 코드를 실제로 실행하는 그래서 OS 커널의 역할을 담당하는 그 쓰레드를 커널 쓰레드라고도 합니다. 그래서 문서 읽으실 때 헷갈리시면 안되요. 어떤 때는 커널 쓰레드가 OS 레벨에서 정의된 우리가 일반적으로 지금까지 알고있었던 의미로 커널 쓰레드를 쓰기도 하구요. 방금처럼 실제로 커널이 하는 일을 수행하는 커널 쓰레드로 의미가 사용될 때오 있습니다. 그래서 문서 읽으실 떄 잘 파악해서 읽으셔야되요.
유저 레벨에서 스케줄링되는 쓰레드는 나중에 배울 코루틴(coroutine)과 관련있으니 잘 기억해주시면 좋아요.
출처:
https://youtu.be/vorIqiLM7jc?si=VdHmteL54pDjBkCo
'Fundamental of CS > : : Computer Architecture' 카테고리의 다른 글
x86-64 CPU 레지스터(Register) 종류, 32bit / 64bit 비교 (0) | 2024.05.31 |
---|---|
The Stack : 인터럽트 메커니즘(Interrupt Mechanism) (0) | 2023.11.13 |
Trap Routines and Subroutines - 시스템 함수와 사용자 함수 (0) | 2023.11.13 |
IO - 입출력장치 (0) | 2023.11.13 |
어셈블리어, 어셈블러 (Assembly Language) (0) | 2023.11.13 |