위 글에서 설명했듯이 비동기 처리는 속도와 효율성 면에서 큰 이점을 가진다.
그리고 Go는 이러한 비동기 루틴 실행을 아주아주아주 쉽게 할 수 있다.
Go 루틴
Go의 루틴은 2KB 스택 공간만을 사용하기 때문에 가볍고, 하나의 코어 당 하나의 OS 스레드만 할당하기 때문에 스레드의 컨텍스트 스위칭 비용이 발생하지 않는다.
go function()
함수를 호출할 때 앞에 go를 붙이면 끝난다.
package main
import (
"fmt"
"time"
)
func main() {
secs := []int{7, 1, 5, 3}
for _, sec := range secs {
Test(sec)
}
}
func Test(sec int) {
time.Sleep(time.Second * time.Duration(sec))
fmt.Println(sec)
}
7
1
5
3
- Test: sec초 후에 sec를 출력
- main: 7, 1, 5, 3초 후에 출력되는 함수를 순차적으로 호출한다.
우선 비동기 처리를 하지 않은 함수는 순서대로 출력된다.
아래 예시는 go루틴을 활용한 비동기 처리를 진행한다.
func main() {
secs := []int{7, 1, 5, 3}
for _, sec := range secs {
go Test(sec)
}
}
비동기 처리를 했으니 효율적으로 코드가 진행될 것 같지만 아무 일도 일어나지 않는다. Go루틴은 main 함수가 실행될 동안만 실행된다. 위 예시의 경우, main 함수에서 Test를 호출한 후 실행할 동작이 없기 때문에 그대로 종료되어 버린다.
func main() {
secs := []int{7, 1, 5, 3}
for _, sec := range secs {
go Test(sec)
}
time.Sleep(time.Second * 10)
}
1
3
5
7
만약 main 함수를 종료하지 않고 10초동안 기다리도록 하면 1 3 5 7 순서로 출력되는 것을 볼 수 있다
Waitgroup
앞에서 비동기가 완료되지 않아도 프로그램이 종료되는 것을 확인하였다. 이러한 상황을 방지하기 위해 비동기 실행 결과 값을 기다리도록 할 수 있다.
- wg.Add: 예약할 go 루틴 갯수를 설정한다.
- wg.Done: 하나의 go 루틴이 종료될 때 호출한다. Add의 카운트를 감소시킨다.
- wg.Wait: 예약한 go 루틴의 갯수만큼 종료되면 넘어간다.
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func SleepAndPrint(i int) {
time.Sleep(time.Duration(i) * time.Second)
fmt.Print(i)
wg.Done()
}
func main() {
iter := 5
wg.Add(iter)
for i := iter; i > 0; i-- {
go SleepAndPrint(i)
}
wg.Wait()
}
공유 자원
비동기 처리에서 메모리 자원을 공유할 때 문제가 발생할 수 있다. 대안으로 Lock을 활용할 수 있다. 자원을 사용하는 루틴이 Lock을 걸어 자원을 독점한 후, 사용이 완료되면 Lock을 해제한다.
var wg sync.WaitGroup
var mutex sync.Mutex
func AsyncFunc(i int) {
mutex.Lock()
defer mutex.Unlock()
// Do something
wg.Done()
}
하지만 mutex를 사용하는 것도 문제가 있다. 자원을 독점하기 때문에 동시성이 실현되지 않고, Dead-Lock 문제가 발생할 가능성이 있다.
채널
채널이름 := make(chan 자료형)
채널은 루틴 간 통신을 위한 큐(Queue)이다. 하나의 채널은 하나의 자료형만을 사용한다.
채널이름 <- 데이터
변수 <- 채널이름
<- 을 이용해 채널로 데이터를 보내거나, 채널로부터 데이터를 받아온다.
func Test(sec int, channel chan int) {
time.Sleep(time.Second * time.Duration(sec))
channel <- sec
}
함수의 파라미터로 통신에 사용할 채널을 전달한다. 그 후 <-를 통해 데이터를 channel로 전송한다.
func main() {
channel := make(chan int)
secs := []int{7, 1, 5, 3}
for _, sec := range secs {
go Test(sec, channel)
}
for i := 0; i < len(secs); i++ {
fmt.Print(<-channel)
}
}
비동기 함수 Test가 총 네 번 실행되기 때문에 채널로부터 네 번 데이터를 받아와야 한다. 위 코드의 경우, <-channel이 그 역할을 수행한다. 네 번 데이터를 받아와 출력한 뒤에 main 함수가 종료된다.
Chan 크기
채널은 버퍼를 가진다.
func main() {
channel := make(chan int)
go AsyncFunc()
channel <- 3
fmt.Println("Hello World!")
}
만약 채널이 꽉 차게 되면 값을 가져갈 때까지 대기한다. 따라서 위 코드는 영원히 종료되지 않는다.
func main() {
channel := make(chan int, 5)
go AsyncFunc()
channel <- 3
fmt.Println("Hello World!")
}
make를 이용해 채널 버퍼의 크기를 지정할 수 있다.
예제 - 비동기 request
package main
import (
"fmt"
"net/http"
)
type res struct {
name string
status int
}
func main() {
names := []string{
"google",
"naver",
"reddit",
"facebook",
}
channel := make(chan res)
for _, name := range names {
go getStatus(name, channel)
}
for i := 0; i < len(names); i++ {
ret := <-channel
fmt.Printf("%s: %d\n", ret.name, ret.status)
}
}
func getStatus(name string, channel chan res) {
url := fmt.Sprintf("https://www.%s.com/", name)
resp, _ := http.Get(url)
channel <- res{name: name, status: resp.StatusCode}
}
reddit: 403
naver: 200
google: 200
facebook: 200
Go는 http.Get을 통해 request를 받아 오는 함수를 기본 패키지(net/http)로 제공한다.
위 코드의 실행 결과, 먼저 완료된 결과를 먼저 보여주는 것을 확인할 수 있다. 이러한 방식으로 request 대기 시간을 효율적으로 사용해 전체 실행 시간을 줄일 수 있다.