본문 바로가기
Study/Go

[Go] 고루틴(Goroutine) 알고쓰기(1) - 동시성, 병렬성 프로그래밍 그리고 고루틴

by _royJang 2021. 9. 1.

그냥 가져다 쓰면 되는줄 알았는데 공부할게 정말 많은 부분이었다. 간단히 기억하기 위해 포스팅을 하려 했으나 생각보다 모르는 부분이 많았고 공부할 가치가 있다 판단하여 조금 깊게 들여다 보았다.

동시성? 병렬성?

무어의 법칙을 아는가? 인텔의 공동 창업자인 무어가 반도체가 1년에 2배씩 빨라 질 것을 예언하였고 어느 시기까지는 들어 맞았다. 하지만 발열량과 같은 물리적 한계에 부딪혔고 더이상 무어의 법칙이 적용되기 어려운 시기까지 도달하게 됐다. 하지만 컴퓨터와 같은 기기들은 계속해서 빨라졌다. 해답을 찾은 것이다. 바로 반도체 하나의 성능을 높일 수 없으니 여러개의 반도체를 사용하기로...!(

공부하다 재밌어서.. + 연관 있음

)

동시성

컴퓨터의 코어가 하나(싱글 코어)일 때를 생각해 보자. 우리는 꽤 오래 싱글 코어의 컴퓨터를 사용해 왔다. 그 시기에도 우리는 컴퓨터에서 인터넷을 하며 메신저를 사용할 수 있었다. 그 이유는 무엇일까? 바로 CPU가 매우 빠르게 다른 일을 번갈라 하며 마치 동시에 많은 일을 처리하는 것 처럼 보이게 하기 때문이다. 이를 동시성이라 한다. 동시에 일어나는 것은 아니지만 매우 빠른 속도로 동시에 일어나는 것 처럼 보이는 논리적 개념이다.

병렬성

무어의 법칙이 물리적 한계에 부딪히자 새로운 파훼법이 나왔다하였다. 그것이 바로 여러개의 CPU를 장착하는 멀티코어라 말했었다. 이러한 멀티 코어의 기기에서 사용 가능한 CPU들은 물리적으로 같은 시간대에 다른 일을 하게 되는데 이를 병렬성이라 한다.

고루틴이란?

Go lang에서 동시성, 병렬성 프로그래밍을 위해 사용하는 경량(輕量) 쓰레드.

쓰레드면 쓰레드지 경량 쓰레드는 뭐냐?

가벼운 무게를 가진 쓰레드란 뜻이다. OS의 쓰레드보다 다른 언어에서 지원하는 쓰레드 보다 고루틴이 가볍다는 뜻이다.

고루틴은 어떻게 동작하는가?

고루틴은 쓰레드에 런타임을 두어 동작한다. 그리고 고루틴이 Blocking 된다면 대기 중인 고루틴이 할당되어 동작하게 된다.

쓰레드와 고루틴의 차이

메모리 소비

고루틴은 2KB, 쓰레드는 1MB의 메모리를 소비한다. 이는 무시할 수 없는 큰 차이이다.(500배 ㄷㄷ)

설치와 철거 비용

쓰레드는 OS로부터 리소스를 요청하고 작업이 끝나면 리소스를 돌려줘야 하기 때문에 큰 설치 비용과 철거 비용이 든다. 하지만 고루틴은 런타임에서 만들어지고 파괴되는 작업들이 매우 저렴하다.

Context Switching 비용

쓰레드의 경우 context switching이 일어날 시 16개의 레지스터가 save/restore 되야 한다.
하지만 고루틴은 오직 3개의 레지스터만이 save/restore되기 위해 필요로 한다.

이러한 차이로 고루틴을 사용한 동시성, 병렬성 프로그래밍을 계획한다면 개발자는 고루틴의 개수에 딱히 신경을 쓰지 않아도 될것이다.

고루틴 기본 사용 방법

코드

package main

import "fmt"

func printer(val int) {
    for i := 0; i < 3; i++ {
        fmt.Println(val)
    }
}

func main() {
    go printer(1)
    go printer(2)
    fmt.Println("hello")

}

헤헤 나도 고루틴 써봐야지 > <

실행 결과

어라 내가 원한건 이런게 아니었는데?
1
1
1
2
2
2
hello
이렇게 출력되야 하는게 아니야?

아니다. 기본적으로 고루틴의 동작은 메인 고루틴이 종료되면 서브 고루틴은 따라서 종료되게 된다. 어떤 방법이 있을까? 간단하다.

코드

func main() {
    go counter(1)
    go counter(2)
    fmt.Println("hello")
    for {
    }
}

실행 결과

메인 고루틴인 메인 함수를 죽이지 않으면 된다.
그를 위해 무한루프를 실행시킨 모습이고 원하는 대로 잘 동작한 모습을 볼 수 있다. 순서는 어떤것이 먼저 올지 알 수 없다.
사실 잘 이라고 하긴 그렇다. 종료되지않는 굉장히 잘못 작성한 코드이니..
그렇다면 어떻게 코드를 짜야할까? 이것도 간단하다.

코드

package main

import (
    "fmt"
    "sync"
)

func printer(val int) {
    for i := 0; i < 3; i++ {
        fmt.Println(val)
    }
}

func main() {
    wg := sync.WaitGroup{} // WaitGroup 생성
    go func() {
        wg.Add(1)
        defer wg.Done()
        printer(1)
    }()
    go func() {
        wg.Add(1)
        defer wg.Done()
        printer(2)
    }()
    fmt.Println("hello")
    wg.Wait()

}

실행 결과


강제로 종료해 줄 필요가 없는 코드가 완성됐다.

코드 설명

sync 라이브러리의 WaitGroup 객체를 만들어 준다. 이 객체에는 Add, Done, Wait 함수가 존재한다.

  • Wait : WaitGroup이 가진 값이 0이 될 때 까지 기다린다.
  • Add : 입력된 값 만큼을 WaitGroup이 가진 값을 증가 시킨다.
  • Done : WaitGroup이 가진 값을 1 감소시킨다. Add(-1)과 같다 생각하면 된다.

익명 함수를 통해 printer함수를 실행 시키는 모습을 볼 수 있다. 실행 전 wg의 값을 +1 시킴으로써 현재 실행 중인 고루틴의 개수가 하나가 있다는 것을 알린다고 생각하면 된다.
defer를 통해 함수가 종료된다면 Done함수를 실행시켜 고루틴이 종료됨을 알린다.
Wait함수는 이 wg가 가진 값이 0이 되지 않는다면 메인 고루틴을 종료시키지 않을 것이다.