Golang Goroutine, Channel, I/O 그리고 Scheduler 이해해보기

Posted on Feb 7, 2019

Go언어를 요즘에 진지하게 생각하고 계속해서 자료를 찾아보며 익히고 있다.

가장 흥미있는 부분은 Goroutine scheduler 구현과 I/O시스템을 어떻게 만들었을까인데, 아직은 소스코드를 뜯어 읽고 하지는 못하지만, 그냥 내가 만들었다면 아마 이렇지 않을까 하는 정도의 추측을 갖고 반대로 예제들을 만들어보며 확인해보고 있다.1

우선 현재의 추측은 다음과 같다.

  1. GOMAXPROCS 같은걸 이용해서 실행시간에는 필요한만큼만 최소한으로만 OS Thread을 시작하는듯. 2
    1. 너무 많은 스레드가 있어도 어차피 Context switching 비용만 늘어나고 별 의미는 커녕 더 나쁠 수 있으니까.
  2. Goroutine들은 Go runtime의 Scheduler이 서로 실행기회를 나눠준다.
    1. 실행기회를 다른 goroutine에 yield하는 방법은,
    2. I/O operation을 실행하거나
    3. Channel에 Receive/Send할때
  3. 위 (2.2)에서 I/O operation이 Async I/O으로 구현되었을거라고 생각.
    1. 왜냐하면, 그래야 blocking 안되고, Go scheduler으로 실행이 넘어가고,
    2. Go scheduler은 I/O event loop을 통해서 적절하게 다음에 실행할 Goroutine을 결정할 수 있을테니까.

예제 프로그램 코드

실행결과, 결론, 생각

예상한대로,

  1. 강제로 GOMAXPROCS=1으로 설정하고 실행했고,
  2. go printer()에서 I/O실행이나 time.Sleep 같은거 안하고, 그냥 무한루프3
    1. 반대로 말해서, 위 func printer()의 루프 안에서 time.Sleep, fmt.Println 중 하나라도 실행을 하도록 하면, Go scheduler이 기회를 얻는다.
  3. go timer(..)이 실행기회를 받지 못함.
    1. 시작은 되지만,
    2. time.After(..)을 통해서 타임아웃하여 종료처리를 못하게됨. Race condition.

현실적으로는 무한루프에서 어떤 I/O을 수행하거나 time.Sleep이라도 한다면 싱글코어만을 활용하는 경우에도 goroutine이 적절하게 스케쥴링될테니 문제가 없겠지만, goroutine들이 어떤 방식으로 스케쥴링되는지 이해하기 좋은 기회였던거 같아.

결국, Go와 같은 Goroutine, Channel만이 중요한게 아니라, Go runtime이 제공하는 I/O들도 Node.js와 같이 비동기적으로 내부적으로 처리될거고, 이 이벤트루프와 스케쥴러가 잘 연동되어 있을거라 상상할 수 있다.

이런 Goroutine, Scheduler의 특성을 잘 이해하고 있지 못하다면, 단순히 어떤 고루틴에서 I/O등이 없이, CPU연산만 열심히 하는 무한루프를 만들고 한다면 정상적으로 동작하지 않는 애플리케이션을 만들고 의아할 수 있으리라 생각.

그리고 더 무섭게도 이 애플리케이션은 다음과 같은 특성을 갖게 될거임:

  1. Println이라도 하나 찍어보면 잘찍히고, 그게 들어가면 또 잘동작하게됨. (I/O연산이니까.)
  2. 다른 머신(코어가 더 있거나, GOMAXPROCS이 더 크게 설정될)에서 실행한다면 똑같은 바이너리가 또 제대로 동작함.

…ㅎㅎㅎ



  1. 이미 이런 방식의 CSP 동시성은 과거 Clojure의 core.async을 통해 익혀서 익숙. 그리고 이걸 뜯어서 이해 하는것도 나쁘지 않을거 같긴하다. ↩︎

  2. https://golang.org/pkg/runtime/ ↩︎

  3. 그냥 x += 1 같은 순수하게 CPU만 사용하는 연산들도 마찬가지로 Scheduler에 yield하지 못한다. ↩︎