[Go] Concurrency: 고루틴 누수 방지하기
고루틴 자체가 다른 언어들의 비동기 시스템과 비교해서 가볍고 단순하다보니, 그만큼 가볍게 써서 누수가 나는 경우가 꽤 많다.
고루틴 누수는 심각한 문제로, 고루틴이 계속 줄줄 새다보면 다른 고루틴 프로세스가 사용해야할 리소스를 낭비하거나, 종국에는 stackoverflow 등으로 프로그램을 터뜨릴 수도 있기 때문이다.
Best Practice: 부모에게 제어권 주기
고루틴 사용에 있어서 가장 안정적인 구조는, 부모가 자식 고루틴을 정리할 수 있게끔 구성하는 것이다.
그럼 부모 고루틴이 종료될때, 혹은 더 이상 해당 자식 고루틴이 필요없다고 판단할 경우에 자동으로 정리되게끔 할 수 있기 때문이다.
기본 원칙은 이렇다.
고루틴 함수의 첫번째 매개변수에는 항상 해당 프로세스를 강제로 종료시킬 수 있는 채널 done을 받도록 한다.
그리고 for-select 루프에서 done이 종료되거나 메세지가 오면 강제로 리턴하도록 구성하는 것이다.
기본 로직은 나머지 select case에서 구현한다.
부모 고루틴에서는 자식 고루틴을 생성하되, 제어용 채널을 만들어서 넘겨주고, 해당 채널을 통해서 언제든 자식 고루틴을 정리할 수 있도록 한다.
그럼 저기서 close(done)을 했을때, 자식 고루틴은 종료 메세지를 받고 return을 하게 된다.
이렇게.
고루틴을 활용함에 있어서 이러한 원칙을 항상 지키도록 노력하면 고루틴 누수 문제를 최소화할 수 있다.
아래는 전체 코드다.
package main
import (
"fmt"
"time"
)
func main() {
doWork := func(done <-chan any) {
defer fmt.Println("doWork done")
fmt.Println("doWork start")
for {
select {
default:
fmt.Println("do something...")
time.Sleep(1 * time.Second)
case <-done:
return
}
}
}
done := make(chan any)
go doWork(done)
// 5초 대기
time.Sleep(5 * time.Second)
close(done)
// 자식 고루틴이 종료될때까지 대기...
time.Sleep(1 * time.Second)
}