최근 Go언어를 공부하면서 고루틴이라는것을 처음 들었다. 하지만 개념을 정확히 짚지않고 무작정 코드부터 이해하려고 하다보니 코드가 점점 더 이해가지않아서 고루틴에 대해서 알아보고자한다.
고루틴(Go routine)이란?
고루틴은 Go언어에서 사용하는 경량 스레드와 비슷한 개념으로, 효율적인 동시성 프로그래밍을 위해 고안되었다.
고루틴을 사용하면 많은 수의 함수를 동시에 실행할 수 있다.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
위의 예시에서 main함수는 두 개의 고루틴(main, say)를 실행한다.
go say("world")부분이 새로운 고루틴을 시작하고, 동시에 say("hello")가 실행된다.
world
hello
hello
world
world
hello
hello
world
world
hello
만약에 위 코드의 go say("world")에서 go를 없애면 어떻게될까?
package main
위와 동일
func main() {
say("world")
say("hello")
}
이렇게 되면 예상하다시피 say("wolrd")라는 함수가 먼저 실행이 된 후, 실행이 끝나면 say("hello")가 실행된다.
world
world
world
world
world
hello
hello
hello
hello
hello
이렇게 우리는 go 키워드를 사용하게 되면 함수가 동시적으로 실행된다는 것을 알았다.
고루틴의 작동방식
고루틴이 어떻게 스케줄링되는가?
Go언어의 고루틴 스케줄러는 M:N 스케줄링을 사용하여 고루틴을 OS 스레드에 할당한다.
Go 스케줄러의 목표는 효율적으로 CPU를 사용하면서 동시에 실행되는 고루틴을 관리하는 것이다.
고루틴 스케줄러의 주요 구성 요소
- M (Machine)
- OS 스레드를 나타낸다.
- 각 M은 CPU 코어에서 실행될 수 있는 고루틴을 실행한다.
- P (Processor)
- 로컬 스케줄러 역할을 하는 구조체이다.
- P는 M에게 실행할 고루틴을 제공하며, 실행할 고루틴 목록을 관리한다.
- 시스템의 각 CPU 코어에는 하나의 P가 할당됩니다.
- G (Goroutine)
- 고루틴을 나타낸다.
- 각 G는 독립적인 실행 경로와 스택을 가지며, M에 의해 실행된다.

스케줄링 프로세스
- Run Queue
- 각 P는 실행할 고루틴 목록을 가진 로컬 run queue를 가지고 있다.
- M은 이 run queue에서 고루틴을 가져와 실행한다.
- Work Stealing
- P의 로컬 run queue가 비어 있으면, 다른 P의 run queue에서 고루틴을 ‘훔쳐’올 수 있다.
- 이를 통해 부하를 균등하게 분산시키고 CPU 사용률을 최적화한다.
- Syscall and Blocking
- 고루틴이 시스템 호출을 하거나 블로킹 연산을 수행하면, 해당 고루틴은 블락 상태가 되고 다른 고루틴이 실행된다.
- Global Run Queue
- 시스템 전체에서 사용할 수 있는 글로벌 run queue도 존재한다.
- 새로 생성된 고루틴은 이 글로벌 run queue에 추가되며, P는 필요에 따라 이곳에서 고루틴을 가져올 수 있다.
- Spinning
- P가 고루틴을 실행할 준비가 되었지만 로컬 run queue가 비어있을 때, P는 다른 P의 run queue에서 작업을 훔치려고 시도할 수 있다.
Go의 런타임 스케줄러의 역할
Go의 런타임 스케줄러는 동시성을 관리하고 고루틴이 효울적으로 실행될 수 있도록 하는 중요한 역할을 한다.
1. 고루틴 관리
- 고루틴은 Go에서 경량 스레드와 같은 역할을 하는 실행 단위이다.
- 스케줄러는 수천에서 수백만 개의 고루틴을 관리하며, 이들을 실행할 준비가 되었을 때 CPU에 할당한다.
2. 효율적인 CPU 사용
- 스케줄러는 시스템의 모든 CPU 코어를 효율적으로 활용하기 위해 고루틴을 적절하게 분배한다.
- 이를 위해 M:N 스케줄링 모델을 사용하며, M은 고루틴의 수, N은 OS 스레드 수를 의미한다.
3. 빠른 컨텍스트 스위칭
- 고루틴 간의 컨텍스트 스위칭은 OS 스레드 간의 컨텍스트 스위칭보다 훨씬 빠르다.
- 스케줄러는 고루틴이 블로킹 연산을 수행하거나 채널 통신을 기다릴 때 빠르게 다른 고루틴으로 전환할 수 있다.
4. 동시성 제어
- 스케줄러는 동시에 실행되는 고루틴의 수를 제어하고, 채널을 통한 통신과 동기화를 관리하여 동시성 프로그램이을 단순화한다.
5. 공정한 실행 시간 할당
- 스케줄러는 모든 고루틴에게 공정하게 실행 시간을 할당하기 위해 노력한다.
- 이는 한 고루틴이 CPU를 독점하는 것을 방지하고, 시스템의 반응성을 향상시킨다.
6. 작업 훔치기
- 스케줄러는 작업 훔치기 기법을 사용하여 부하를 균등하게 분산시킨다.
- 한 스레드의 로컬 실행 큐가 비어 있을 때, 다른 스레드의 큐에서 고루틴을 훔쳐와 실행할 수 있다.
7. 데드락 감지
- 스케줄러는 데드락 상황을 감지하고 경고를 제공할 수 있다.
- 예를 들어, 모든 고루틴이 블록되어 있고 작업을 진행할 수 없는 상황을 감지할 수 있다.
고루틴과 스레드의 차이점
고루틴은 스레드와 정말 비슷하지만 다른 차이점을 가지고있다.

고루틴의 장단점
장점
- 매우 작은 메모리 풋프린트를 가지고 있어 수천 개 혹은 그 이상의 고루틴을 동시에 실행할 수 있다
- 새로운 스레드를 생성하는 것보다 훨씬 빠르게 시작된다
- 고루틴 간의 컨텍스트 스위칭은 OS 스레드 간의 컨텍스트 스위칭보다 훨씬 빠르다
- Go의 채널과 함께 사용하면 동시성 프로그래밍을 쉽게 구현할 수 있다
- 필요에 따라 스택 크기를 동적으로 조절할 수 있다
단점
- 블로킹 시스템 콜을 실행하면 해당 OS 스레드 전체가 차단될 수 있다
- 고루틴이 끝나지 않고 계속 실행되는 상태로 남아있는 경우, 메모리 사용량 증가로 이어질 수 있으며, 애플리케이션의 성능에 영향을 줄 수 있다.
- 고루틴 내에서 발생한 패닉은 해당 고루틴 내에서만 처리되며, 다른 고루틴에 영향을 주지않는다
- 얼핏보면 장점 같을 수 있지만, 오류가 났을 때 오류가 발생한 고루틴 내에서만 처리해야하므로 오류 처리가 복잡해진다
나도 아직 Go언어를 시작하는 단계이지만, 고루틴을 잘 이해해서 좋은 코드를 짜보고자 한다.