Go 스케줄러의 비밀 공개 G-M-P 모델 활용
Wenhao Wang
Dev Intern · Leapcell

소개
현대 소프트웨어 개발 영역에서 동시성은 반응성이 뛰어나고 확장 가능한 애플리케이션을 구축하기 위한 초석이 되었습니다. Go는 내장된 고루틴과 채널을 통해 동시 프로그래밍 과제를 해결하는 강력한 언어로 자리매김했습니다. 그러나 Go 동시성 이면의 뛰어남은 표현적인 구문뿐만 아니라 매우 효율적이고 정교한 스케줄러에 있습니다. 이 스케줄러는 수천, 심지어 수백만 개의 고루틴 실행을 투명하게 관리하여 CPU 활용도를 극대화하고 지연 시간을 최소화하는 숨겨진 영웅입니다. Go가 이 놀라운 작업을 어떻게 달성했는지 이해하는 것은 진정으로 성능이 뛰어난 Go 애플리케이션을 작성하려는 모든 개발자에게 중요합니다. 이 기사에서는 Go 스케줄러의 핵심, 특히 기본적인 G-M-P 모델을 살펴보고 그 작동 방식을 명확히 하고 Go의 동시성 능력 뒤에 숨겨진 마법을 공개할 것입니다.
Go 동시성의 기반 GMP 설명
스케줄러의 메커니즘을 분석하기 전에 Go의 동시성 모델의 기반을 형성하는 핵심 구성 요소에 대한 명확한 이해를 확립해 보겠습니다.
- 고루틴 (G): 고루틴은 독립적으로 실행되는 경량 함수 또는 메서드입니다. 더 적은 수의 OS 스레드에 다중화됩니다. 고루틴은 스레드와 유사하지만 생성 및 관리가 훨씬 저렴합니다. 수천 또는 수백만 개의 고루틴이 최소한의 오버헤드로 동시에 실행될 수 있습니다.
- 머신 (M): "스레드"라고도 하는 머신은 운영 체제 스레드를 나타냅니다. 이것은 운영 체제 스케줄러가 보고 디스패치하는 것입니다. Go는 고루틴을 M 스레드 풀에 매핑합니다. 활성 M 스레드의 수는 일반적으로 사용 가능한 CPU 코어 수와 연결됩니다.
- 프로세서 (P): 프로세서는 논리 프로세서 또는 실행 큐입니다. 고루틴의 로컬 스케줄러 역할을 합니다. P는 실행할 준비가 된 고루틴의 로컬 실행 큐를 보유합니다. 각 M은 고루틴을 실행하려면 연결된 P가 필요합니다. P의 수는
GOMAXPROCS
환경 변수에 의해 결정되며 기본적으로 논리 CPU 코어 수입니다.
G-M-P 모델은 고루틴(G)을 논리 프로세서(P)에 바인딩하여 고루틴 실행을 조정하며, 이는 운영 체제 스레드(M)에 의해 실행됩니다. P를 고루틴을 위한 주차장으로, M을 운전자로 생각하십시오. 운전사(M)는 주차장(P)에서 자동차(G)를 가져와 운전합니다. 주차 공간보다 자동차가 더 많으면 일부 자동차는 전역 큐에서 대기해야 할 수 있습니다.
G-M-P 모델 작동 방식
고루틴 스케줄링의 일반적인 흐름을 자세히 살펴보겠습니다.
-
고루틴 생성:
go
키워드를 사용하여 새 고루틴이 생성되면 초기에는 사용 가능한 P의 로컬 실행 큐에 배치됩니다. 로컬 큐가 가득 차 있거나 사용 가능한 P가 없으면 고루틴은 전역 실행 큐로 이동할 수 있습니다. -
고루틴 실행: P에 바인딩된 M은 P의 로컬 실행 큐에서 지속적으로 고루틴을 가져옵니다. M이 고루틴을 실행하면 고루틴이 다음 중 하나를 수행할 때까지 실행됩니다.
- 차단됨 (예: I/O, 뮤텍스 또는 채널 작업 대기).
- 자발적으로 제어권을 양보함 (사용자 영역 코드에서는 덜 일반적임).
- 실행을 완료함.
-
차단 작업: M의 고루틴이 시스템 호출(예: 네트워크 I/O 또는 파일 I/O)에서 차단되면 M은 P에서 분리되고 P는 다른 M에서 가져갈 수 있도록 자유로워집니다. 이를 통해 한 고루틴의 차단 작업이 전체 P를 보류하지 않도록 합니다. 차단 시스템 호출이 반환되면 원래 고루틴은 다시 P를 획득하여 실행을 재개하려고 시도합니다. P를 사용할 수 없는 경우 실행 큐로 다시 배치됩니다.
-
작업 훔치기: P와 연결된 M이 로컬 실행 큐가 비어 있음을 발견하면 단순히 유휴 상태로 있지 않습니다. 대신 다른 P의 로컬 실행 큐에서 고루틴을 "훔치기"하려고 시도합니다. 작업 훔치기라고 하는 이 메커니즘은 모든 사용 가능한 P에 걸쳐 부하 균형을 맞추고 CPU 활용도를 극대화하는 데 중요합니다. 스케줄러는 일반적으로 작업의 균등한 분배를 위해 다른 P 실행 큐의 절반을 훔치려고 시도합니다.
-
전역 실행 큐: 즉시 P를 찾을 수 없거나 작업 훔치기로 인해 고아된 고루틴의 경우 전역 실행 큐가 대체 역할을 합니다. M은 로컬 P의 큐와 다른 P의 큐가 비어 있으면 전역 실행 큐를 확인합니다.
동시성을 설명하는 코드 예제
고루틴을 사용하여 동시 작업의 간단한 예는 다음과 같습니다.
package main import ( "fmt" "runtime" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 고루틴이 완료되면 WaitGroup 카운터를 감소시킵니다. fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Duration(id) * 100 * time.Millisecond) // 일부 작업을 시뮬레이션합니다. fmt.Printf("Worker %d finished\n", id) } func main() { fmt.Printf("Number of logical CPUs: %d\n", runtime.NumCPU()) fmt.Printf("GOMAXPROCS initially set to: %d\n", runtime.GOMAXPROCS(0)) // 현재 GOMAXPROCS를 가져옵니다. // 선택적으로 GOMAXPROCS를 1로 설정하여 병렬성을 줄입니다. // runtime.GOMAXPROCS(1) // fmt.Printf("GOMAXPROCS set to: %d\n", runtime.GOMAXPROCS(0)) var wg sync.WaitGroup numWorkers := 5 for i := 1; i <= numWorkers; i++ { wg.Add(1) // 각 고루틴에 대해 WaitGroup 카운터를 증가시킵니다. go worker(i, &wg) } wg.Wait() // 모든 고루틴이 완료될 때까지 기다립니다. fmt.Println("All workers completed") }
이 코드를 실행하면 다른 슬립 시간을 가지고 있음에도 불구하고 작업자 함수가 종종 병렬로 시작하고 완료되는 것을 관찰할 수 있습니다. 이는 Go 스케줄러가 이러한 고루틴을 사용 가능한 P와 M에 분산시키는 것을 보여줍니다. runtime.GOMAXPROCS(1)
의 주석을 해제하면 하나의 P(및 따라서 사용자 영역 고루틴을 실행하는 하나의 M)만 사용 가능하므로 보다 순차적인 실행을 볼 수 있습니다. 이는 GOMAXPROCS
가 병렬성 수준에 직접적으로 영향을 미치는 방식을 강조합니다.
결론
Go 스케줄러는 독창적인 G-M-P 모델로 동시 프로그래밍의 경이로움입니다. 스레드 관리의 복잡성을 추상화하고 작업 훔치기와 차단 작업의 효율적인 처리와 같은 메커니즘을 활용하여 개발자에게 강력하고 놀랍도록 간단한 동시성 모델을 제공합니다. 고루틴, 머신 및 프로세서 간의 상호 작용을 이해하는 것은 기본 하드웨어를 효과적으로 활용하는 고성능, 확장 가능한 Go 애플리케이션을 작성하는 데 핵심입니다. Go 스케줄러는 고루틴 실행을 효율적으로 조정하여 Go의 동시 프로그래밍을 강력하고 놀랍도록 접근 가능하게 만듭니다.