백엔드 엔지니어 이재혁
Go는... 신세계다... (vs Java VT) 본문
Go 언어... 컴파일 언어인데, GC를 지원한다고?
GC 지원 언어로는 인터프리터나 하이브리드 언어만 알고 있던 나에게는 뭔가 신세계였다.
흥미는 여기서 시작했는데, 깊이 들어가보니 Go는 신세계 덩어리였다.
Go라는 언어에 대해 공부하는 것을 넘어 컴퓨터에 대해 다시 생각해볼 수 있는 좋은 기회였다.
멀티스레딩
Go는 멀티스레딩을 런타임이 관리한다.
이게 무슨 말이냐,
실행하고 있는 프로세스 전체가 이미 스레드풀이라고 보면 되려나?
Go는 개발자가 작업을 던지면 Go가 알아서 멀티스레딩으로 관리해준다.
Java에 비유하면, 스레드풀 관리랑 대기큐 관리를 JVM이 대신 해주는거다.
(Java에도 Virtual Thread라는 경량 스레드가 있지만 구조적 차이로 Go만큼 좋은 경량 스레드는 아니다.)
Goroutine (고루틴)
고루틴은 Go의 경량스레드다.
다른 언어들의 경량스레드와는 수준이 다르다.
Go는 이 경량스레드를 위해서 태어났다고 느껴졌다.
Go 런타임이 경량스레드를 스케줄링한다.
Goroutine 스케줄링?
스케줄링은 OS가 해주는거 아니야? 맞다
그런데 Go는 자신만의 스케줄링 모델을 가지고 있다.
보통 스케줄링이라고 하면, 스레드를 관리하는 것이다. OS가.
Go 런타임의 스케줄링은 Goroutine이라는 경량스레드를 관리하는 것이다.
OS 레벨 스레드 위에 경량 스레드 레이어를 하나 더 얹었다는 느낌
개발자는 이제 스레드를 다루는게 아니라, 경량 스레드를 다룬다.
Goroutine과 OS레벨의 스레드는 서로 M:N 관계로 실행이 된다.
그 상태를 관리하는게 Go 런타임의 스케쥴러다.
Go 런타임?
Go의 런타임은 인터프리터 언어의 런타임과 다르다.
Go는 코드를 기계어로 바꾸기 위해 존재하는 런타임이 아니라
메모리 상태를 관리해주기 위해 런타임이 존재한다.
Go 컴파일러가 이미 바이너리를 제공한다.
즉, 코드는 이미 기계어이고, 런타임은 메모리와 스케쥴 관리를 해준다.
그리고 Go 런타임은 꽤나 가볍다. 그래서 빌드한 바이너리에 포함시킬 수 있는 수준이다.
Go vs Java (경량 스레드 상태 저장 방법)
| 항목 | [Go] Goroutine | [Java] Virtual Thread |
| 스택 소유 | 고루틴마다 별도 스택 | carrier 스레드 스택 빌림 (= OS 스레드) |
| 스택 위치 | 런타임이 직접 관리, 주소 고정 | 힙의 continuation 객체로 표현 |
| 중단 시 | SP/PC 저장만 | 스택 프레임을 힙에 복제 |
| 재개 시 | SP/PC 복원 | 힙에서 스택 프레임 복원 (OS 스레드 스택에 복원) |
| 주소 안정성 | 높음 (고정된 메모리 블록) | 낮음 (GC 이동 가능) |
Go와 Java의 경량 스레드 모두 힙에 스택을 두지만,
Go는 주소가 고정되어 있고, Java는 가변이다.
(Go도 스택 위치가 가끔 바뀔 수 있음. 스택 크기 한도 문제 등 개입이 필요할 때만 Go 런타임이 위치 변경 및 추적)
Java의 VT(Virtual Thread)는 왜 주소를 고정시키지 않았나?
- JVM 모델에 Heap의 일부 객체만 특별 취급하는 방식을 도입하기 어려움 (큰 변경)
→ GC시 힙 객체들의 메모리 위치가 바뀔 수 있다. - JIT이 생성한 네이티브 코드의 호출 경로는 OS 스레드 스택에 강하게 종속됨
(JVM은 OS를 추상화하는 목적으로 설계되어서 JIT이 만든 코드는 OS 레벨에 강결합되어 있을 것으로 추측)
→ 그래서 JIT 코드는 OS 레벨의 스레드를 1:1로 다루는 것만 가능하고, 완전한 사용자 수준 스케줄링 어려움
스택 사용 방법
Go

Go는 경량스레드 컨텍스트 스위칭시 Stack Pointer / Program Counter 등만 바꾸면 된다. (스택을 있는 그대로 사용)
Java
Java Virtual Thread 컨텍스트 스위칭 과정
1. 초기 상태
2. Virtual Thread 1의 상태를 Heap에 저장
3. OS Thread 스택을 비움 (JVM 런타임은 그대로 둠)
4. Virtual Thread 2의 상태를 OS Thread로 복사함

Java의 경우 경량스레드의 상태 정보를 OS 스레드에 불러와서 실행한다.
따라서, Go와 비교해 경량스레드 스택을 저장/로드하는 시간이 추가로 소요된다.
컨텍스트 스위칭이 가벼운 이유
1. 스레드풀 vs 경량스레드
스레드풀: 작업의 상황에 따라 블로킹이 일어날 수 있다. → 컨텍스트 스위칭 발생
경량스레드: 블로킹이 일어난 순간, 다른 경량스레드의 작업을 하도록 한다. → OS 레벨에서는 컨텍스트 스위칭 X
2. 스레드 컨텍스트 스위칭과 비교
- 커널모드 진입 제거
- 커널 모드는 항상 복귀 가능한 상태를 유지해야 함 (시스템 안정성 위해)
→ 중단점마다 저장해야 할 상태가 훨씬 많음
→ 커널 모드에 진입하는 것 자체가 엄청난 비용 - 유저 모드: 너 여기서만 놀아.(프로세스 가상 메모리 공간) 대신 너가 뭘 하든 신경 안쓸게
- 커널 모드: 컴퓨터에 있는거 전부 쓸 수 있게 해줄게. 대신 내가 모든 상태를 추적하고,
문제가 생겨도 시스템 전체가 멈추지 않도록 항상 복구 가능한 상태를 유지할 거야
- 커널 모드는 항상 복귀 가능한 상태를 유지해야 함 (시스템 안정성 위해)
커널모드를 생략할 수 있는 이유
운영 체제는 바뀐 스레드가 같은 프로세스인지, 다른 프로세스인지 예측하지 못한다.
매번 바뀔 때마다 같은 프로세스인지 검사하기엔 그 비용이 더 크다.
그래서 그냥 항상 프로세스가 바뀐다고 가정하고 커널 모드를 무조건 들어가는 것이다.
경량 스레드: 프로세스 내부로 범위를 제한한 유저 모드 스케줄링
경량 스레드는 컨텍스트 스위칭해도 같은 프로세스임을 보장한다.
그래서 항상 프로세스에게 주어진 유저 모드 공간만 활용하면 된다는 것이 보장된다.
프로세스 사이를 넘나들지 않으니 커널 모드를 진입할 이유가 사라졌다.
고루틴 상태 저장
Go는 컨텍스트 스위칭할 때 필요한 것들을 다음과 같은 구조체로 저장한다.
// Goroutine을 나타내는 구조체
type g struct {
stack stack // 스택
stackguard0 uintptr
stackguard1 uintptr
m *m // m (고루틴이 실행될 쓰레드)
sched gobuf
...
}
// Goroutine의 상태를 저장하는 구조체 (컨텍스트 스위칭시 상태 유지를 위함)
type gobuf struct {
sp uintptr // 스택 포인터
pc uintptr // 프로그램 카운터
g guintptr // 고루틴 번호
ctxt unsafe.Pointer
ret uintptr // 리턴 포인터
lr uintptr // 리턴 레지스터 (고루틴 종료 후 돌아갈 장소)
bp uintptr
}
출처: 링크
OS에서 할당한 가상의 메모리 주소를 직접 접근한다.
Stack Pointer, Program Counter 등의 메모리 위치를 그대로 저장 가능
(런타임이 다루고, 보통 개발자가 다루지는 않음)
Go의 스케줄링 모델

G (Goroutine): 실행 단위. 사용자 코드
M (Machine): OS 스레드. 실제 커널 스레드와 직접 대응
P (Processor): Go 런타임이 관리하는 논리적 실행 컨텍스트 (경량 스레드풀 느낌)
LRQ(Local Run Queue): P에서 실행하기 위한 대기큐 (경량 스레드풀의 대기큐)
GRQ(Global Run Queue): 모든 P가 공유하는 전역 대기큐 (LRQ가 비거나 꽉 찼을 때 사용하는 완충용 큐)
Goroutine 및 Go 런타임의 스케줄링 방법에 대해 더 자세히 알고 싶다면, 이 글을 참고해보라.
동기화
알아서 멀티스레드로 관리해준다는 말은 유혹적이지만, 그게 정말로 잘 되려면
실행 순서를 잘 제어하고, 동기화 처리도 적절히 해줘야 한다.
Go가 그런 것까지 알아서 해주지는 못한다.
작업 흐름을 제어하기 위한 "채널"이라는 개념은 다음 글에서 작성하겠다.
참고
Go 언어의 메모리 관리에 대해 아주 깊이있는 내용을 담은 글: Go의 메모리 관리 글 참고
한국어로 이 수준의 깊이 있는 글을 제공해주는 것이 너무 감사하다. Go에 관심 있다면 꼭 보시길...
Go/Golang Memory Management
이번엔 Go의 메모리 관리에 대해 정리해보려 합니다 Go가 1.17이 Release되는 현재 시점에서 해당 내용에 관해 국내에서 정리된 문서가 없는 것 같습니다 (몇가지 번역 문서는 존재하는것 같습니다)
syntaxsugar.tistory.com