관리 메뉴

밤 늦게까지 여는 카페

golang의 goroutine은 어떻게 2만개까지 생성할 수 있는 걸까요? 본문

For Fun/Go-lang

golang의 goroutine은 어떻게 2만개까지 생성할 수 있는 걸까요?

Jㅐ둥이 2025. 7. 6. 23:17
반응형

안녕하세요. 이제 본격적으로 더위가 찾아온 것 같습니다. 건강 관리에 각별히 유의하시기 바랍니다.

이번에는 golang의 goroutine에 대해서 공부한 내용을 정리하려고 합니다.
제가 근 6년 동안 golang으로 서비스를 개발하면서 go routine을 정말 많이 사용했는데 동작 원리를 정확히 모르더라고요.

이번 기회에 공부하고 내용 정리해보겠습니다!


1. Kernel level thread, User level thread

일단 go routine에 대해 공부하기 전에 Kernel level thread와 User level thread에 대해 알아보겠습니다.
thread는 가장 작은 실행 단위로 cause 시간을 할당 받아 작업을 수행합니다.

Kernel level thread는 우리가 흔히 아는 스레드로 커널 모드를 거쳐서 스레드가 생성되고 전환되기 때문에 속도가 느린 편입니다.

 

User level thread는 커널 모드로의 진입 없이 사용자 레벨 라이브러리에서 관리해주기 때문에 스레드 생성 및 속도가 빠른 편입니다.

또한, 메모리도 유동적으로 관리할 수 있기 때문에 훨씬 부담이 적습니다.

이렇게 보면 User level thread가 무조건 좋아보이지만 커널 모드로 진입하지 않는 것이 문제의 복잡도를 매우 증가시킵니다.

가장 쉽게 발견할 수 있는 문제들은 다음과 같습니다.

  • I/O 작업과 같이 블로킹 시스템 콜이 호출되면 먹통이 되는 현상
  • 멀티코어를 활용하지 못함

2. 그러면 go routine은 뭐에요?

아마 짐작하셨겠지만 go routine은 golang runtime이 관리해주는 User level thread 입니다.

 

go routine은 블로킹 시스템 콜이 호출되었을 때 다른 go routine이 실행되고 멀티코어도 잘 활용할 수 있습니다.

golang runtime에서 적절히 관리해주기 때문이죠.

 

2.1. I/O 멀티플렉싱 기법을 이용한 블로킹 시스템 콜 개선

golang runtime은 파일 디스크립터를 생성할 때 epoll, kqueue와 같은 I/O 멀티플렉싱 기법을 이용해서 해당 파일 디스크립터를 지켜봅니다.

https://github.com/golang/go/blob/6c3b5a2798c83d583cb37dba9f39c47300d19f1f/src/internal/poll/fd_unix.go#L55
https://github.com/golang/go/blob/6c3b5a2798c83d583cb37dba9f39c47300d19f1f/src/internal/poll/fd_poll_runtime.go#L38

 

그리고 해당 파일 디스크립터를 읽을 때 epoll을 이용해서 효율적으로 go routine을 실행할 수 있게 됩니다.

https://github.com/golang/go/blob/6c3b5a2798c83d583cb37dba9f39c47300d19f1f/src/internal/poll/fd_unix.go#L160

 

2.2. GMP 스케줄러를 이용한 멀티코어 활용

GMP 스케줄러에서 GMP가 의미하는 바는 다음과 같습니다.

  • G (Goroutine): go 키워드로 생성된 하나의 고루틴입니다. 스택, 명령어 포인터 등 함수 실행에 필요한 정보를 가집니다.
  • M (Machine): OS 스레드를 의미하며, 커널에 의해 관리되는 실행 주체입니다. M은 P로부터 G를 할당받아 실행합니다.
  • P (Processor): P는 실행 가능한 G들의 큐(Run Queue)를 가지고 있으며, M에게 G를 전달하고 실행을 스케줄링합니다.

 

go 키워드가 실행되면 다음과 같이 go routine이 관리됩니다.

1. go routine(G)이 생성되면 P의 큐에 삽입됩니다.

2. M은 P의 로컬 큐에 들어 있는 G를 하나씩 실행합니다.

3. 만약 M에서 G를 실행하다가 블로킹이 발생하면 P에 있는 다른 G를 실행하게 됩니다.

 

모든 흐름을 정확히 파악한 것은 아니지만 실제 코드 흐름은 다음과 같습니다.

 

0. 컴파일러가 go 키워드를 찾아서 newproc 함수로 대체합니다.

1. newproc 함수에서 G를 생성하고 P의 큐에 삽입합니다.

2. schedule 함수 내부의 findRunnable에서 G를 찾아서 실행합니다.

3. schedule 함수 내부의 execute 함수로 go routine이 실행되는데 시스템 콜이 끝나면 exitsyscall이 호출되면서 다시 schedule 함수로 돌아옵니다.

반응형