Goroutine, GMP Model
Goroutine과 GMP 모델: Go 런타임의 스케줄링 아키텍처
Go 언어의 가장 강력한 특징 중 하나는 경량 동시성 스레드로 불리는 Goroutine 이다.
수십만 개의 Goroutine 을 동시에 실행할 수 있는 이유는 Go 런타임의 정교한 스케줄러 덕분이다.
물론 스케줄러의 탁월함에 대해서는 이견의 여지가 많지만 여기서는 다루지 않는다
Go 에서는 M:N 스케줄링 모델과 GMP 아키텍쳐를 차용했다.
M:N 스레딩 모델 비교
프로그래밍 언어의 동시성 모델은 user-level 스레드와 kernel-level thread 를 어떻게 매핑하느냐에 따라 크게 세가지로 분류된다.
1:1 모델
각 유저 스레드가 커널 스레드에 1대 1로 매핑된다.
Pros
-
True parallelism
-
구현이 단순하다
Cons
-
컨텍스트 스위칭 비용이 비싸다. 1대1 이니까 커널 수준에서 발생한다.
-
스레드 생성/소멸 비용이 커진다.
N:1 모델 (Green Threads)
여러 사용자 스레드가 하나의 커널 스레드에서 실행된다
Pros
-
빠른 컨텍스트 스위칭 (사용자 공간에서 처리된다)
-
낮은 메모리 사용량
Cons
-
하나의 스레드가 블로킹되면 전체가 블로킹된다
-
True parallelism 은 아님
M:N 모델
M 개의 Goroutine 을 N 개의 OS 스레드에 매핑하는 하이브리드 방식이다.
Goroutine 은 OS 스레드가 아니다. OS 스레드 위에서 실행된다.
Pros
-
대규모 동시성이 가능하다
-
낮은 메모리 오버헤드
-
빠른 컨텍스트 스위칭
Cons
-
복잡한 스케줄러가 필요하다
-
블로킹 시스템 콜 처리 비용
GMP 아키텍쳐
Go 런타임 스케줄러는 세 가지 핵심 구성 요소로 이루어져 있다.
G(Goroutine) + M(Machine/OS Thread) + P(Processor)
Goroutine 은 사용자 공간에서 실행되는 경량 태스크이다.
Machine(OS Thread)은 실제 Goroutine 을 실행하는 OS 스레드를 말한다.
머신이 고루틴을 실행하려면 바느시 P에 바인딩 되어야한다.
최대 개수는 기본 10,000개인데, debug.SetMaxThreads 로 설정도 가능하다.
P는 논리 프로세서로, G와 M을 연결하는 다리 역할이다. 그 유명한 GOMAXPROCS 환경 변수가 이것의 개수를 말한다.
스케줄링 동작 원리
Go 스케줄러는 협력형과 선점형의 하이브리드 방식으로 동작한다.
협력형 스케줄링 (Pre-Go 1.14)
Goroutine 이 자발적으로 실행권을 양보하는 시점:
-
함수 호출시
-
채널 작업
-
go키워드 실행 -
블로킹 시스템 콜
-
GC Safe point
문제점: 함수 호출 없이 오래 실행되는 CPU 집약적 코드는 P를 독점하는 경우가 생겼다.
// Pre-Go 1.14: 이 코드는 P를 독점
for i := 0; i < 1000000000; i++ {
// 함수 호출 없음
}
선점형 스케줄링 (Go 1.14+)
비동기 선점을 도입하여 위 문제를 해결했다. 동작방식은:
-
sysmon이라는 고루틴이 백그라운드에서 모니터링 -
고루틴이 10ms 이상 실행되면 선점 대상으로 표시
-
M의 OS 스레드에 SIGURG 시그널 전송
-
시그널 핸들러가 레지스터 저장 후 스케줄러로 제어 이동
SIGURG: 소켓에 긴급한 데이터가 도착했을 때 해당 소켓의 소유자 프로세스에 전송되는 비동기 신호
Work Stealing 알고리즘
말 그대로 훔쳐오는 알고리즘이다. P가 실행할 고루틴이 없을 때 다른 P의 작업을 훔친다.
-
runnext 확인
-
(없으면) Local Run Queue (LRQ) 에서 pop
-
(없으면) Global Run Queue (GRQ) 확인
-
(없으면) 다른 P의 LRQ 에서 절반을 훔쳐옴 (Randomly)
-
(없으면) Network poller 확인 (I/O 완료된 Goroutine)
-
(없으면) M을 Park (유휴 상태로 전환)
