고루틴(Goroutine)는 go언어에서 가장 유용한(그리고 가장 많이 사용하는)기능 중 하나일 거다. 고루틴은 go언어에 내장된(built-in) 기능이다. 개발자는 표준라이브러리나 외부라이브러리의 의존 없이 언어자체로 동시성(concurrency)애플리케이션을 개발 할 수 있다. 고루틴을 가지고 프로그램을 만들다 보면, 고루틴의 종료를 기다리거나 고루틴이 실행하는 것을 기다려야 하는 경우가 있다.
예를 들어 문자열 변환을 처리하기 위한 몇 개의 고루틴을 만들었다고 가정해보자. 애플리케이션은 해당 고루틴들이 모두 끝나는 걸 기다렸다가 종료해야 한다. 중간에 종료하면 안될 거다. 어떤 방법을 사용해야 할까 ?
답 : sync.WaitGroup 를 사용한다.
이 문서는 고루틴과 채널에 대한 기본적인 이해를 하고 있다고 가정하에 작성됐다.
Sleep로 기다리기
많은 블로그와 튜토리얼 문서에서 아래와 같이 time.Sleep를 이용하는 코드들을 찾아볼 수 있다.
package main
import (
"fmt"
"time"
)
func main() {
messages := make(chan int)
go func() {
time.Sleep(time.Second * 3)
messages <- 1
}()
go func() {
time.Sleep(time.Second * 2)
messages <- 2
}()
go func() {
time.Sleep(time.Second * 1)
messages <- 3
}()
go func() {
for i := range messages {
fmt.Println(i)
}
}()
time.Sleep(time.Second * 5)
}
출력 결과는 아래와 같다.
3
2
1
4개의 고루틴이 실행 중이다. 고루틴이 끝나기 전에 main 함수가 끝나면 안되기 때문에, 산술계산을 해서 5초 정도를 기다리게 했다. 가장 길게 실행되는 고루틴이 3초가 될테니, main 함수가 먼저 끝나는 일은 없을 것이다.
이런 코드는 고루틴에 대한 개념을 학습하기 위한 목적에서 (그리고 코드 몇 줄 추가하기 귀찮아서) 만든 코드지 실제 서비스에서 작동하는 애플리케이션을 위한 코드는 아니다. 대부분의 경우 고루틴의 종료 시점을 알 수 없기 때문이다.
작업종료 메시지를 받기
sync.WaitGroup를 호출하지 않고, 각 고루틴으로 부터 작업종료 메시지를 받는 방법이다. 작업종료 메시지를 받기 위한 채널은 물론 만들어야 한다. 위의 Sleep를 이 방법으로 수정해보자.
package main
import (
"fmt"
"time"
)
func main() {
messages := make(chan int)
done := make(chan bool)
go func() {
time.Sleep(time.Second * 3)
messages <- 1
done <- true
}()
go func() {
time.Sleep(time.Second * 2)
messages <- 2
done <- true
}()
go func() {
time.Sleep(time.Second * 1)
messages <- 3
done <- true
}()
go func() {
for i := range messages {
fmt.Println(i)
}
}()
for i := 0; i < 3; i++ {
<-done
}
}
실제 코드에서는 defer문을 이용해서 done <- true 를 실행 할 수 있도록 해야 할 것이다. 첫번째 코드보다는 나아졌지만 for 루프문을 구성해야 한다. 채널도 하나 새로 만들어야 하고 별로 깔끔하지 않다.
sync.WaitGroup를 이용하기
고루틴을 기다리는 일반적인 방법은 sync 패키지의 WaitGroup 구조체를 이용하는 거다.
WaitGroup은 고루틴들이 끝날 때까지 기다린다. (고루틴을 호출한)메인 고루틴은 기다려야할 고루틴의 갯수를 설정한다. 이제 설정한 갯수만큼의 고루틴이 종료되기 전까지 (기다리면서)블럭 할 수 있다.
sync.WaitGroup는 아래의 순서로 사용하면 된다.
sync.WaitGroup 구조체로 부터 새로운 인스턴스를 만든다. 인스턴스의 이름은 wg로 하겠다.
wg.Add(n)을 호출한다. n은 기다려야할 고루틴의 갯수다. 예를 들어 wg.Add(1)을 하면 하나의 고루틴 종료를 기다린다.
각 고루틴에 defer wg.Done()코드를 넣어서, 고루틴이 종료 할 때 wg.Done()를 실행하도록 한다.
메인 고루틴은 wg.Wait()를 호출해서 고루틴의 종료를 기다린다.
앞의 예제를 sync.WaitGroup를 사용하도록 수정했다.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
messages := make(chan int)
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
time.Sleep(time.Second * 3)
messages <- 1
}()
go func() {
defer wg.Done()
time.Sleep(time.Second * 2)
messages <- 2
}()
go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
messages <- 3
}()
go func() {
for i := range messages {
fmt.Println(i)
}
}()
wg.Wait()
}
좀더 그럴듯한 예제를 만들어보자. 나는 3개의 URL에서 데이터를 읽어서 저장하는 프로그램을 만들려고 한다. 나는 HTTP 클라이언트 요청이 얼마나 걸릴지 모르고, 어떤 경쟁조건도 만들기 싫었으므로 sync.WaitGroup로 코드를 만들기로 했다.
package main
import (
"io/ioutil"
"log"
"net/http"
"strings"
"sync"
"time"
)
func main() {
urls := []string{
"https://www.joinc.co.kr/w/man/12/golang/BehaviorOfhannels",
"https://www.joinc.co.kr/w/man/12/golang/wait",
"https://www.joinc.co.kr/w/man/12/golang/HTTPPerf",
}
var wg sync.WaitGroup
wg.Add(len(urls))
for _, url := range urls {
time.Sleep(time.Second)
go func(url string) {
defer wg.Done()
res, err := http.Get(url)
if err != nil {
log.Fatal(err)
} else {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
} else {
urls := strings.Split(url, "/")
ioutil.WriteFile(urls[len(urls)-1], body, 0644)
}
}
}(url)
}
wg.Wait()
}
Sleep로 기다리기
작업종료 메시지를 받기
sync.WaitGroup를 이용하기
참고
Recent Posts
Archive Posts
Tags