본문으로 바로가기

Go Routine의 원리

참고 : stonzeteam.github.io/How-Goroutines-Work/

참고 : tech.ssut.me/goroutine-vs-threads/

 

동시성 처리하면 보통 멀티 스레드가 떠오른다. 이런 상식답게 자바, C++과 같은 언어는 쓰레드를 사용하여 동시적인 처리를 한다. 그러나 Go는 GoRoutine을 사용하여 동시적 처리를 한다. 정확히는 한 프로세스로 모든 코어를 활용하면서 적은 메모리를 사용하는 방식으로 동작한다.

 

직접 스레드를 사용하는 것보다 고루틴을 사용하는 것이 좋은 이유는 간단하다.

프로세스던 프로세스를 쪼갠 스레드던 OS의 스케줄링에 따라서 움직이기 때문에 어떤 동작을 하기 전의 준비 동작들(전처리)가 필요하고 이로서 발생하는 비용이 크다. 반면 Go는 OS에 리소스를 요청하지 않고(OS 스케쥴러는 고루틴을 모른다) Go 스케쥴러가 스케쥴링을 합니다.

 

Go 스케쥴러는 G, M, P(goroutine, machine, processor)로 구성되어있다고 하는데, 스케쥴러 레벨까지 내려가기에는 아직 시기 상조고, Go를 사용해보면서 공부해야 겠습니다.

 

 

 

그래서 고루틴이 일반 스레드 방식과 비교해 무엇이 좋은가?

GoRoutine의 장점은 다음과 같습니다.

 

고루틴은 생성하는데에 많은 메모리를 필요로 하지 않습니다. 오직 2kB의 스택 공간만 필요로 합니다. 고루틴을 할당하고 필요에 따라 힙 저장 공간을 확보하여 사용합니다.
반면 Java에서의 스레드는 시작과 동시에 1Mb(500배 더 큼)의 스택 공간을 요구로 하고 스레드와 스레드 사이의 보호 공간(guard page)까지 필요로 합니다. 게다가 더 많은 스레드를 생성해낼 수록 heap 공간이 더 적어지는 문제점이 존재합니다.
쓰레드는 거대한 설치와 철거 비용을 가집니다. 왜냐하면 쓰레드는 OS로부터 리소스를 요청해야 하고 작업이 끝나면 리소스를 돌려줘야 하기 때문입니다. 이 문제의 차선책으로는 쓰레드의 Pool을 유지하는 것입니다. 대조적으로, 고루틴은 런타임에서 만들어지고 파괴되는 작업들이 매우 저렴하다. 그렇기 때문에 Go는 고루틴의 메뉴얼 관리를 지원하지 않습니다.

 

 

rumtime

NumCPU 갯수가 1개면 동시성

go routine를 돌리고나니 숫자가 +1 된 것을 확인할 수 있다.

func main() {
	fmt.Println("OS\t\t", runtime.GOOS) // windows
	fmt.Println("ARCH\t\t", runtime.GOARCH) // amd64
	fmt.Println("CPU\t\t", runtime.NumCPU()) // 12
	fmt.Println("Goroutines\t", runtime.NumGoroutine()) // 1

	go foo()
	bar() // bar가 끝나면 foo가 어찌 되었던 일단 끝난다. 기다리게 하려면 sync 시켜야 한다.
	fmt.Println("CPU\t\t", runtime.NumCPU())
	fmt.Println("Goroutines\t", runtime.NumGoroutine()) // 2
}

 

 

개념은 위와 같다. 구체적인 코드로는 어떻게 사용할 수 있을까? 다음과 같다.

 

아래와 같은 코드는 apollo, spuk을 동시에 실행한다. (즉, 20초가 아니라 10초가 걸립니다)

고루틴을 통해 동시성 처리가 단순히 'go'를 붙인 것만으로도 가능해진다.

func main() {
	go rocketCount("apollo") // 10초
	rocketCount("spuk")      // 10초
}


func rocketCount(rocket string) {
	for i := 0; i < 10; i++ {
		fmt.Println(rocket, "is sexy", i)
		time.Sleep(time.Second)
	}
}

 

아래와 같이 고루틴을 사용하면 main 함수에서 처리할 내용이 없어 곧바로 프로그램이 종료된다.

func main() {
	go rocketCount("apollo") // 10초
	go rocketCount("spuk")      // 10초
}

 

 

주의

- 동시적 != 병렬적

 

  • 동시성: 싱글 코어에서 스레드를 여러 개 생성해서 겉으로 보기에는 동시에 실행되는 것처럼 보입니다.
  • 병렬성: 멀티 CPU 코어에 나눠서 작업을 동시에 처리함.
동시성(Concurrent) 병렬성(parallel)
동시에 실행되는 것 같이 보이는  실제로 동시에 여러 작업이 처리되는 것
싱글 코어에서 멀티 쓰레드(Multi thread)를 동작 시키는 방식 멀티 코어에서 멀티 쓰레드(Multi thread)를 동작시키는 방식
한번에 많은 것을 처리 한번에 많은 일을 처리
논리적인 개념 물리적인 개념

(출처)

blog.golang.org/waza-talk

 

Concurrency is not parallelism - The Go Blog

Andrew Gerrand 16 January 2013 If there's one thing most people know about Go, is that it is designed for concurrency. No introduction to Go is complete without a demonstration of its goroutines and channels. But when people hear the word concurrency they

blog.golang.org

 

싱글 코어가 아닌 멀티 코어를 사용하고 싶다면 runtime.GOMAXPROCS 를 사용하면 됩니다.

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	fmt.Println("cpu 갯수", runtime.NumCPU()) // 현재 제 컴퓨터에서는 12 코어 잡히네요
	runtime.GOMAXPROCS(runtime.NumCPU() - 6)

	for i := 0; i < 100; i++ {
		go hello()
	}
	time.Sleep(time.Second * 3)
}

func hello() {
	fmt.Println("hello")
}

 

 

 

 

 

- 동시성 프로그래밍을 구현했다고 해서 자동 동기화(sync)가 되는 것은 아니다.

 

아래와 같은 코드는 5초만 실행되고 main 함수가 종료되기 때문에 rocketCount는 전부 실행되기 못하고 중간에 중단된다.

func main() {
	go rocketCount("apollo") // 10초
	go rocketCount("spuk")   // 10초
	sleepFiveSec() // 5초
}

func rocketCount(rocket string) {
	for i := 0; i < 10; i++ {
		fmt.Println(rocket, "is sexy", i)
		time.Sleep(time.Second)
	}
}

func sleepFiveSec() {
	time.Sleep(time.Second * 5)

}

 

동기화를 위해 기다리게 하려면 stdlib 중 sync를 사용해야한다.

pkg.go.dev/sync

 

sync · pkg.go.dev

This example fetches several URLs concurrently, using a WaitGroup to block until all the fetches are complete. Code: package main import ( "sync" ) type httpPkg struct{} func (httpPkg) Get(url string) {} var http httpPkg func main() { var wg sync.WaitGroup

pkg.go.dev

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체