Skip to content

Goroutine

고루틴은 GO언어에서 프로그램의 동시성을 쉽게 구현하고 기존의 단순 스레드 기반의 구현에 비해 효율적인 동작을 수행해 내기 위해 만든 작업단위 이다.

고루틴의 장점#

아래 내용에 들어가기에 앞서서 스레드에 관해 정리해야할 부분이 있다. CPU의 멀티스레딩에 활용되는 용어인 스레드와 아래에서 설명될 OS영역에서 다뤄지는 스레드는 서로 지칭하는 대상이 다르다. CPU의 스레드는 한개의 코어를 OS에게 여러개로 인식시켜 동작하도록 하는 하드웨어 영역의 개념이고, 아래에서 계속하여 언급할 스레드는 OS 하위의 소프트웨어 영역에서 CPU의 작업단위로 지칭되는 용어이다.

메모리 소비#

고루틴은 스레드에 비해 더 작은 메모리만 필요로 함. 고루틴 생성에는 2KB의 스택만 필요로 하고 필요에 따라 힙을 사용.

반면 스레드는 스택 간 메모리 Guard _page_을 하는 공간을 포함하여 1MB 정도의 스택을 필요로 함.

→ 서버로 들어오는 요청당 하나의 고루틴을 만드는 것에 대해 무리가 없지만, 요청당 하나의 스레드의 경우 OOM으로 이어지기 쉽다.

생성 및 소멸에 필요한 비용#

스레드는 OS에 리소스(메모리)를 요청하고 다 사용하면 반환해야 하기 때문에 상당한 생성, 소멸 비용이 들어감. 스레드 풀을 이용하면 어느 정도는 문제를 해결할 수 있기는 함.

이와 대조적으로 고루틴은 Go 런타임에 의해 생성 및 소멸되며 매우 적은 비용으로 이루어짐.

컨텍스트 스위칭 비용#

기본적으로 프로세스 레벨의 컨텍스트 스위칭이 프로세스 내부 스레드간의 컨텍스트 스위치 비용보다 비싸다. 이유는 컨텍스트 전환 시 처리해야 하는 데이터의 양이 훨씬 많기 때문이다.

컨텍스트 스위칭 image

고루틴에서 컨텍스트 스위칭(?)이 발생하는 경우에는 스레드의 경우보다도 훨씬 적은 정보를 처리하기 때문에 더 비용이 적다는 점은 말할 필요도 없다.

컨텍스트 스위칭 이라는 단어는 기본적으로 프로세스/스레드 레벨의 작업단위를 cpu코어가 연산하며 전환할때 이용되는 용어이기 때문에 고루틴에 사용하기에는 부적절한 부분이 있긴 하다.

하지만, 중요한 점은 단순히 전환에 필요한 정보의 양이 적다는 수준이 아니다.

고루틴이 프로세스/스레드의 컨텍스트 스위칭과 근본적으로 빠를 수 밖에 없는 부분은 스위칭 과정에서 커널<->유저 모드의 전환이 일어나지 않는다는 점이다.

아래는 컨텍스트 스위칭이 일어날때 기본적으로 OS가 수행하는 로직을 대략적으로 설명한 내용이다.

  • 유저프로세스 A 가 커널모드로 진입하면서 유저모드 레지스터 컨텍스트를 A 의 커널스택에 저장
  • 프로세스 A 는 커널컨텍스트를 가진상태로 Schedule 을 호출
  • A 의 커널컨텍스트가 메모리에 저장되고 프로세스 B 의 저장된 커널컨텍스트를 레지스터에 복원
  • B 의 커널컨텍스트가 메모리에 저장되면서 B 의 유저컨텍스트를 레지스터에 복원하면서 유저모드로 리턴

Go Runtime Scheduler#

goroutine 은 Runtime Scheduler 에 의해 관리된다. Runtime Scheduler 는 Go 프로그램이 실행되는 시점에 함께 실행되며, goroutine 을 효율적으로 스레드에 스케줄링 시키는 역할을 수행한다.

아래와 같은 원칙을 가지고 동작한다.

  • 커널 스레드는 비싸기 때문에 되도록 작은 수를 사용한다.
  • 많은 수의 goroutine 을 실행하여 높은 Concurrency 를 유지한다.
  • N 코어 머신에서, N 개의 goroutine 을 Parallel 하게 동작시킨다.

스케줄러 동작 컨셉들#

본격적인 동작 컨셉들을 알아보자.

1.Reuse threads

Runtime Scheduler(이하 스케줄러)는 고루틴이 필요할 때 스레드를 생성한다. 그런데 생성된 스레드에 더이상 실행할 고루틴이 없게 된다면? 스레드를 생성/종료할 때에도 시스템콜이 필요하게 되며, 자원의 할당/반납의 과정에서 부하가 발생한다. 스케줄러는 이렇게 생성된 고루틴이 종료된 뒤에도 일정시간 반납하지 않고 idle 상태로 관리하다가 새로운 고루틴에게 빠르게 할당해 줄수 있다.

그런데 만약 모든 스레드가 고루틴을 처리하는 동안에도 고루틴이 계속해서 만들어진다면 어떻게 될까?

2.Limit threads accessing runqueue

GO에서 스케줄러가 생성할 수 있는 스레드 최대 수는 기본적으로 10000개로 되어 있다. 하지만 그것이 동시에 실행 가능한 고루틴의 수는 아니다. 위 GMP모델에서 스레드가 M에 해당했다면, M 에 G를 할당하여 동작을 관리하는 실질적인 기능을 수행하는 P 는 기본적으로 CPU 코어 갯수로 제한된다(runtime.GOMAXPROCS를 활용하여 수를 조절할 수도 있긴 하다). 그렇기 때문에 위에서 우려했던, 고루틴이 무한정 과도하게 많은 스레드를 생성하여 할당받는 상황은 걱정하지 않아도 된다.

3.Distributed runqueues

LRQ는 P에 하나씩 할당된다. 그렇기 때문에 동시에 여러 고루틴이 여러 스레드에 할당되는 과정을 스케줄링 할때 GRQ만 존재하게 되었을때 발생할 수 있는 mutex(스케줄락)을 최소한 시킬 수 있다.

그리고 특정 LRQ가 빠르게 소비되어 더이상 수행할 고루틴이 없게 된다면 연결된 스레드를 놀게 하거나 종료시키는 것이 아니라 다른 P이 LRQ의 고루틴들을 절반 뚝 떼어와 가져오거나, GRQ의 고루틴을 가져와서 실행하는 똑똑한 로직도 있다.

참고로, 과거 GO가 1.0 버전이었을 때에는 GMP모델이 아닌 GM 모델이었는데, 당연히 LRQ가 별도로 존재하지 않아서 위에서 말하는 mutex로 인한 성능저하가 꽤 있었다고 한다.

4.Blocking System call

위에서 설명하는 내용에 따르면, 동시에 실행될 수 있는 고루틴과 스레드의 수는 GOMAXPROCS에 따라 결정되는 P 에 의해서 결정되는 것이니까 동시에 실행되는 G = M갯수 = P갯수 일까? 아니다

G와 M은 P보다 많은 수가 동시에 할당되어 실행 될 수 있다. 그렇다면 어떨 때 P보다 많은 G와 M이 존재할 수 있는 걸까?

바로 system call이 고루틴 내에서 수행되어 고루틴이 할당된 스레드가 blocking되었을 때이다.

P는 M이 블로킹 된 것을 감지하면, idle 상태의 M을 가진 P의 LRQ로 본인이 소유한 LRQ의 고루틴을 할당하거나 idle상태의 M이 없다면 새로운 M(스레드)를 생성하여 고루틴을 할당한다.

실제로 아래와 같은 코드를 실행하면, 1002개의 스레드가 생성되어 실행되는 것을 볼 수 있다.

Go에 구현된 라이브러리들 중 system call로 간주되어 스레드를 블로킹 할 것 같은 기능을 수행하는 대부분의 것들(socket I/O, waiting timer, file I/O …등)은 OS에서 제공하는 select, poll, epoll, kqueue 등의 기능을 통해 I/O 멀티플렉싱을 수행하고, 덕분에 블록킹 하지 않고 system call을 수행해 낼 수 있도록 설계되어있다.

exam#

fmt.Println("Goroutines 갯수: ", runtime.NumGoroutine())
fmt.Println("내 컴퓨터의 코어 갯수: ", runtime.NumCPU())
fmt.Println("고루틴이 사용하는 코어 갯수: ", runtime.GOMAXPROCS(0))

고루틴 생성시 약 2KB의 스택 메모리 공간만 필요로 하며, 필요에 따라 힙 메모리 공간을 사용하기도 한다 반면에 쓰레드는 쓰레드가 사용할 메모리 공간과 각 메모리 간의 경계 역할을 하는 Guard Page라고 불리는 메모리 영역과 함께 포함하여 약 1Mb의 메모리 공간을 소모하여 생성된다 따라서 Golang 기반의 서버에서는 요청 1건 당 1개의 고루틴을 생성 하도록 만들 수 있지만, 요청 1건 당 1개의 쓰레드를 할당하는 다른 언어 기반의 서버는 앞선 방식으로 쓰이게 되면 결국에는 OOM(OutOfMemory) 이슈의 원인이 될 것이다. 이는 쓰레드를 사용하는 언어기반으로 만들어진 서버가 지속적으로 쓰레드 생성 요청을 받게 된다면 마주하게 될 이슈다(예로들어 Java, C++), 그래서 이러한 언어 환경에서는 쓰레드를 미리 만들어 두고 재활용하는 형태인 쓰레드 Pool을 사용하고 해당 쓰레드 비용에 대한 문제를 풀어내려는 노력이 있을 것이다.