Featured image of post Golang: 12. Go 루틴

Golang: 12. Go 루틴

Go루틴(goroutine)은 Go 런타임이 관리하는 Lightweight 논리적 (혹은 가상) 쓰레드이다. Go에서 go 키워드를 사용하여 함수를 호출하면, 런타임시 새로운 goroutine을 실행한다.

고루틴은 비동기적으로 함수루틴을 실행하므로, 여러 코드를 동시에 실행하는데 사용된다.

고루틴은 OS 쓰레드보다 훨씬 가볍게 비동기 Concurrent(동시성) 처리를 구현하기 위하여 만든것으로, 기본적으로 Go 런타임이 자체 관리한다.

  • Go 런타임 상에서 관리되는 작업단위인 여러 고루틴들을 하나의 OS 쓰레드 1개로도 실행되곤 한다.
    • 고루틴들은 OS 쓰레드와 1대1로 대응되지 않고, Multiplexing(다중화)으로 훨씬 적은 OS 쓰레드를 사용한다.
  • 메모리 측면에서도 OS쓰레드가 1MB 스택을 갖는 반면, 고루틴은 이보다 훨씬 작은 몇 KB 스택을 갖는다(필요시 동적으로 증가함).

Go 런타임은 Go루틴을 관리하면서 Go 채널을 통해 Go 루틴간의 통신을 쉽게 할 수 있도록 했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
 
import (
    "fmt"
    "time"
)
 
func say(s string) {
    for i := 0; i < 10; i++ {
        fmt.Println(s, "***", i)
    }
}
 
func main() {
    // 함수를 동기적으로 실행
    say("Sync")
 
    // 함수를 비동기적으로 실행
    go say("Async1")
    go say("Async2")
    go say("Async3")
 
    // 3초 대기
    time.Sleep(time.Second * 3)
}

main 함수를 보면, 먼저 say() 함수를 동기적으로 호출하고, 다음으로 동일한 say() 함수를 비동기적으로 3번 호출하고 있다. 첫번째 동기적 호출은 say() 함수가 완전히 끝났을 때 다음 문장으로 이동하고, 다음 3개의 go say() 비동기 호출은 별도 고루틴들에서 동작하면서, 메인루틴은 계속 다음 문장을 실행한다. 고루틴들은 비동기이므로 처리 순서가 일정하지 않으므로 프로그램 실행시 마다 다른 출력 결과를 나타낼 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Sync *** 0
Sync *** 1
Sync *** 2
Sync *** 3
Sync *** 4
Sync *** 5
Sync *** 6
Sync *** 7
Sync *** 8
Sync *** 9
Async3 *** 0
Async3 *** 1
Async3 *** 2
Async3 *** 3
Async3 *** 4
Async3 *** 5
Async3 *** 6
Async3 *** 7
Async3 *** 8
Async3 *** 9
Async2 *** 0
Async2 *** 1
Async2 *** 2
Async2 *** 3
Async2 *** 4
Async1 *** 0
Async1 *** 1
Async1 *** 2
Async1 *** 3
Async1 *** 4
Async1 *** 5
Async1 *** 6
Async1 *** 7
Async1 *** 8
Async1 *** 9
Async2 *** 5
Async2 *** 6
Async2 *** 7
Async2 *** 8
Async2 *** 9

익명함수 Go루틴

고루틴은 익명함수에 대해 사용할 수도 있다. 즉, go 키워드 뒤에 익명함수를 바로 정의하는 것으로, 익명함수를 비동기로 실행하게 된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    // WaitGroup 생성. 2개의 Go루틴을 기다림.
    var wait sync.WaitGroup
    wait.Add(2)
 
    // 익명함수를 사용한 goroutine
    go func() {
        defer wait.Done() //끝나면 .Done() 호출
        fmt.Println("Hello")
    }()
 
    // 익명함수에 파라미터 전달
    go func(msg string) {
        defer wait.Done() //끝나면 .Done() 호출
        fmt.Println(msg)
    }("Hi")
 
    wait.Wait() //Go루틴 모두 끝날 때까지 대기
}

첫번째 익명함수는 간단히 Hello를 출력하는데, 이를 고루틴으로 실행하면 비동기적으로 그 익명함수를 실행하게 된다. 두번째 익명함수는 파라미터를 전달하는 예제로 익명함수에 파라미터가 있는 경우, go 익명함수 바로 뒤에 파라미터를 함께 전달하게 된다.

여기서 sync.WaitGroup을 사용하고 있는데, 이는 기본적으로 여러 고루틴들이 끝날 때까지 기다리는 역할을 한다. WaitGroup을 사용하기 위해서는 먼저 Add() 메소드에 몇 개의 Go루틴을 기다릴 것인지 지정하고, 각 고루틴에서 Done() 메서드를 호출한다.(여기서는 defer 사용)

그리고 메인루틴에서는 Wait() 메서드를 호출하여, Go루틴들이 모두 끝나기를 기다린다.

다중 CPU 처리

go는 디폴트로 CPU 1개를 사용한다. 여러 개 고루틴을 만들더라도, CPU 1개에서 작업을 시분할하여 처리한다. 만약 머신이 CPU 여러개를 가진 경우, Go 프로그램을 다중 CPU에서 병렬처리(Perallel)하게 할 수 있는데, 병렬처리를 위해서는 아래와 같이 runtime.GOMAXPROCS(CPU수) 함수를 호출하여야 한다.

  • CPU 수는 Logical CPU 수를 가리킨다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main
 
import (
    "runtime"  
)
 
func main() {
    // 4개의 CPU 사용
    runtime.GOMAXPROCS(4)
 
    //...
}

동시성과 병렬성은 비슷하게 들리지만 전혀 다른 개념이다.