golang에 대해 접하고 좀더 나아가 고루틴이라는 것을 알고 써보게 되면 한번씩 경량화된 쓰레드라는 문구를 꼭 보게 됩니다.
A goroutine is a lightweight thread managed by the Go runtime.
하지만 왜 경량화된 것인지 무심코 넘어가기 마련인데요. 이번 글에서는 그 부분에 대해 살펴보고자 합니다.
본문에 들어가기 앞서 몇 가지 짚고 넘어가야 할 것이 있는데요.
먼저 위에서 말씀드린 고루틴은 OS에서 나오는 스레드랑은 1:1로 매칭되진 않습니다. 그 내용은 뒤에서 좀 더 나올 것이라 우선 이 정도만 언급하고 넘어가겠습니다.
그리고 경량화된 스레드라고 했을 때는 그 비교 대상이 있어야겠죠. 무엇과 비교했을 때 가볍다라고 할 수 있을 것이니까요. 본문에선 많이 쓰는 언어 중 하나인 java의 스레드와 비교하며 살펴보도록 하겠습니다. java의 스레드는 고루틴과 달리 OS 스레드와 1:1 매칭이라고 볼 수 있습니다.
스택 사이즈
java의 스레드는 고정된 크기(1MB)로 할당됩니다. 아무리 적은 공간만 사용된다고 하더라도 1MB는 기본적으로 사용되기 때문에 비효율적입니다. 이와 달리 고루틴의 경우 기본 4KB로 할당되며 필요에 따라 유동적으로 스택의 사이즈가 늘어나는 구조입니다.
생성, 소멸 비용과 컨텍스트 스위칭 비용
java의 스레드는 OS 스레드와 매칭되기 때문에 그만큼 쓰레드 내에 저장하는 데이터의 양이 많습니다. 그와 달리 고루틴은 몇 가지의 데이터만 저장됩니다. 그렇기 때문에 생성과 소멸의 비용이 적게 드는 장점이 있습니다.
이와 더불어서 이런 차이는 컨텍스트 스위칭에도 영향을 미치게 됩니다. 먼저 컨텍스트 스위칭에 대한 영향을 말하기 전에 그 상황과 고루틴이 스레드에 할당되는 과정에 대해 살펴보겠습니다.
위 서문에서 고루틴은 OS 쓰레드와 1:1 매칭은 아니라고 했는데요. 그 이유는 고루틴은 go 런타임 내에서 스케쥴링되며 실제로 OS 쓰레드에는 그룹화된 쓰레드들이 할당되기 때문입니다.
그런데 스레드 개수가 코어 개수보다 많아질 때는 코어가 여러개의 스레드를 돌아가면서 수행하게 됩니다.
이때 위 그림에서 본 스레드와 고루틴의 데이터 크기의 차이는 컨텍스트 스위칭 비용에 큰 영향을 미치게 됩니다. 하나의 스레드의 일부분을 수행하고 다음 스레드로 넘어가게 될 때 현재 진행 상황에 대해 저장하고 그 다음 시작할 스레드에서 읽고 시작하는 과정에서 비용이 크기 때문입니다.
위 내용에서 고루틴이 많이 뜨더라도 실제로 스레드의 개수는 고루틴 개수만큼 증가하지 않는다는 점 알게되었을 것입니다. 하지만 상황에 따라 항상 그렇지는 않는데요. 그런 점에서 유의해야 할 부분이 있습니다.
위의 그림과 같이 OS 스레드에서 system call과 같이 스레드가 블로킹 되는 시점에는 다른 OS 스레드를 생성하여 사용하게 됩니다. 그렇기 때문에 OS 쓰레드가 블로킹될 때는 OS 쓰레드가 증가될 수 있음을 충분히 인지하며 혹시 무분별한 OS 스레드 블로킹이 사용되고 있다면 피해야 할 것입니다.