본문 바로가기
golang

goroutine 파헤치기

by marble25 2023. 9. 23.

Go 언어에서는 내부적으로 고루틴이라는 경량 쓰레드를 사용해서 Concurrency를 구현한다.

하지만 그동안은 정확히 쓰레드와 어떤 차이가 있고 어떻게 고루틴이 동작하는지 정확히 알지 못한 채 사용하기만 해서 이번 기회에 이를 정리해두고자 한다.

 

참고 문서

https://blog.nindalf.com/posts/how-goroutines-work/

https://syntaxsugar.tistory.com/entry/GoGolang-Scheduler

https://ssup2.github.io/theory_analysis/Golang_Goroutine_Scheduling/

https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html

Goroutine vs Thread

Memory 사용량

goroutine을 생성할 때에는 오직 2kb의 스택 메모리만 필요로 한다. 이 메모리는 heap을 할당하고 해제하면서 늘어나거나 줄어들게 된다. 반면, 쓰레드는 1Mb부터 시작한다. 하나의 쓰레드 메모리와 다른 것들을 구분짓는 guard page를 포함하기 때문이다.

만약 request를 받는 서버가 request별로 하나의 고루틴을 생성한다면 큰 문제가 생기지 않겠지만, 하나의 쓰레드를 생성한다면 Out of Memory 에러가 나기 쉽다. Java 뿐만 아니라 concurrency의 수단으로 OS 쓰레드를 사용하는 모든 프로그램에 해당된다.

Setup & Teardown cost

쓰레드는 OS로 자원을 요청하고 반환받아야 하기 때문에 Setup과 Teardown에 cost가 많이 든다. 반면 고루틴의 경우에는 런타임으로부터 생성되고 삭제되기 때문에 상당히 cheap하게 operation을 수행할 수 있다.

Switching cost

쓰레드가 블락되면 다른 쓰레드가 스케쥴된다. 스레드는 선점적으로 스케쥴되고, 쓰레드 전환 때 모든 register를 저장하고 복원해야 한다. 반면 고루틴의 경우에는 3개의 레지스터만 저장되고 복원되면 된다 - Program Counter, Stack Pointer, DX.

Go Scheduler

Go에서는 내부적으로 G-M-P 모델을 사용해서 고루틴을 스케쥴링한다.

  • G: 고루틴. 고루틴을 구성하는 논리적 구조체이다.
  • M: Machine. OS Thread를 의미하고, 실제 OS Thread가 아닌 논리적 구현체이다.
  • P: Processor. 실제 프로세서가 아닌 논리적 프로세서이다.
    • Go 환경변수인 GOMAXPROCS만큼의 프로세서를 가질 수 있다.
      • 기본적으로 GOMAXPROCS는 logical CPU의 개수만큼 설정되어 있다.
    • P는 G를 M에 할당시키는 역할을 수행한다.
  • LRQ: Local Run Queue. Processor마다 하나의 LRQ를 가지고 있다.
    • 새로 생성된 Goroutine은 일반적으로 고루틴을 생성한 프로세서의 LRQ에 들어가게 된다.
  • GRQ: Global Run Queue. LRQ에 할당되지 못한 고루틴들이 모여있다.
    • Executing 상태가 된 고루틴은 최대 10ms까지 동작하고, 동작이 끝난 고루틴은 Waiting 상태가 되어 GRQ로 이동된다.
    • 고루틴 생성시 고루틴을 생성한 프로세서의 LRQ가 가득찬 경우 GRQ에 저장된다.

작동 원리

1. Goroutine State

Goroutine의 상태를 간략하게 나타낸다면 3가지로 나타낼 수 있다.

  • Waiting: Goroutine이 stop된 상태고, 외부의 어떤 signal을 기다리는 상태이다. os를 기다릴 수도 있고, sync call을 기다릴 수도 있다.
  • Runnable: Goroutine이 instruction을 실행할 수 있는 상태이다. Runnable 상태의 고루틴이 많은 경우 오랫동안 기다려야 할 수 있다.
  • Executing: Goroutine이 M에 할당되어 실행하고 있는 상태이다.

2. Run Queue

LRQ는 일반적인 Queue가 아닌 FIFO와 size가 1인 LIFO가 결합된 형태를 가지고 있다.

처음 Enqueue되면 LIFO에 저장된 후 그 이후에 FIFO로 enqueue된다. 반대로 Dequeue하는 경우 LIFO queue가 먼저 빠지고 그 후 FIFO queue에서 dequeue된다. 이렇게 설계된 이유는 생성된 고루틴을 바로 실행하여 프로세서의 캐시나 스택을 바로 사용할 수 있도록 하기 위함이다.

3. Async System Calls

System call을 async하게 처리하기 위해서, Network Poller를 사용한다. Network Poller라고 이름붙여진 이유는, 주로 사용되는 이유가 네트워크 IO를 처리하기 위함이기 때문이다.

네트워크 system call 같은 async call이 필요하기 때문에 Goroutine-1은 network poller로 이동해서 async system call을 처리한다. Goroutine-1이 다른 쓰레드로 이동했기 때문에 M은 다른 고루틴을 받아올 수 있다. LRQ에 있던 Goroutine-2가 실행된다. 네트워크 요청이 끝나면 network poller에 있던 Goroutine-1이 LRQ 맨 마지막에 들어가게 된다.

4. Sync System Calls

File-based system call 같은 경우 Sync 방식으로 동작해야 한다.

Goroutine-1이 sync system call을 호출한 경우 scheduler는 기존에 사용되던 M1을 detach 후 새로운 M2를 P에게 할당한다. 그 후, context-switch하여 LRQ에 있던 Goroutine-2를 실행한다. system call이 끝나면 G1은 다시 LRQ 맨 마지막에 들어가게 된다.

5. Work Stealing

runtime.schedule() {
    // only 1/61 of the time, check the global runnable queue for a G.
    // if not found, check the local queue.
    // if not found,
    //     try to steal from other Ps.
    //     if not, check the global runnable queue.
    //     if not found, poll network.
}

Golang Scheduler는 LRQ에 고루틴이 없는 경우 다른 곳에서 고루틴을 가져온다.

  • 61번 중 한번 꼴로 GRQ에 남은 고루틴이 있는지 확인한다.
  • LRQ에 남은 고루틴을 실행한다.
    • LRQ에 남은 고루틴이 없으면, 다른 Processor의 LRQ로부터 절반을 가져온다.
      • 다른 Processor도 없으면, GRQ에서 고루틴을 가져온다.
        • GRQ에도 없으면, 네트워크 폴링한다.

이때, locality를 위해 생성된지 3ms 이내의 고루틴은 건드리지 않는다.

6. Fairness

고루틴의 공평한 실행을 위해 여러 기법들을 적용하고 있다.

  • Thread: 1개의 고루틴이 쓰레드를 오래 점유하는 것을 막기 위해 10ms의 time slice만 부여한다. 실행중이던 고루틴은 GRQ로 들어간다.
  • LRQ: 2개의 고루틴이 번갈아가면서 LRQ에 저장되고 실행된다면, FIFO에 존재하는 고루틴이 실행되지 않을 수 있다. Scheduler는 LIFO에 있는 고루틴은 10ms timeout이 초기화되지 않고 상속되도록 만들어져 LIFO 부분을 한 고루틴이 10ms이상 점유하지 않도록 한다.
  • GRQ: 61번 Goroutine Scheduling을 하면 1번은 GRQ goroutine을 실행해준다.
    • 61이 정해진 이유는 실험적으로 성능이 좋았던 값의 범위 안에서 prime number를 골랐다고 한다.
  • Network Poller: 백그라운드 작업을 위해 존재한다.

'golang' 카테고리의 다른 글

Go 상속 vs 구성  (1) 2023.10.01
Go context 파헤치기  (0) 2023.09.29
Formatting in golang  (0) 2023.09.03
Internal package in golang  (0) 2023.09.03
Go 언어를 활용한 분산 서비스 개발  (0) 2023.07.23