Posts 고루틴 누수 방지
Post
Cancel

고루틴 누수 방지

개요

고루틴을 사용하면서 데드락을 경험하거나, 어떻게 다른 채널을 종료시킬지 고민할 때가 있다. 지금 소개하는 패턴을 사용하게 되면, 부모 고루틴과 자식 고루틴 사이에 어떻게 close 신호를 전달할지 이해할 수 있다.

문제 발생

  • 고루틴들은 런타임에 의해 가비지 컬렉션되지 않으므로 Memory Leak이 발생할 가능성이 있다.
  • 메모리상에서 고루틴들이 종료되게 제어할 수 있는 방법이 필요하다.
    • 그렇다면, 부모 고루틴들이 자식 고루틴들을 종료할 수 있게 만들어 주면 된다.

읽기 채널의 고루틴 제어

변경 전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main(){
    doWork := func(strings <-chan string) <-chan struct{} {
        completed := make(chan struct{})
        go func(){
            defer close(completed)
            for s := range strings {
                fmt.Println(s)
            }
        }()
        return completed
    } 
    terminated := doWork(nil)

    <-terminated
    //DeadLock 발생
    fmt.Println("Done")
}
  • 위와 같은 경우 nil 채널에 string이 아무것도 들어오지 않기 때문에 doWork 고루틴은 프로세스가 종료될 때 까지 아무것도 수행하지 않는다.
  • 심지어 terminated 채널에 아무것도 들어오지 않기 떄문에 데드락 상태가 발생한다.
  • 먄약 위 main 프로세스가 계속 돌아가는 상태라면 doWork 고루틴은 메모리를 계속 잡고 있게 된다. -> Memory Leak

변경 후

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main(){
    doWork := func(done <-chan struct{}, strings <-chan string) <-chan struct{} {
        completed := make(chan struct{})
        go func(){
            defer close(completed)
            for s := range strings {
                fmt.Println(s)
            }
        }()
        return completed
    }
    done := make(chan struct{})

    terminated := doWork(done,nil)
    
    go func(){
        time.Sleep(time.Second)
        close(done)
    }

    <-terminated
    fmt.Println("Done")
}
  • 부모 고루틴이 자식 고루틴에게 cancel 신호를 보낼 수 있게 신호를 설정하였다.
  • 이 상황에서는 위와 똑같이 strings 채널에 아무것도 들어가지 않더라도, 부모 고루틴에서 자식 고루틴을 제어할 수 있는 done 채널이 존재하기 떄문에 고루틴 누수를 제거할 수 있다.

쓰기 채널의 고루틴 제어

변경 전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    func main(){
        newRandStream := func() <-chan int{
            randStream := make(chan int)
            go func(){
                defer fmt.Println("newRandStream closure exited")
                defer close(randStream)
                for {
                    randStream <- rand.Intn(100)
                }
            }()
            return randStream
        }
        randStream := newRandStream()
        for i:=0; i<3; i++{
            fmt.Println(<-randStream)
        }
    }
    /*
    OUTPUT
    81
    87
    47
    */ 
  • 위와 같은 상황에서는 쓰기 채널을 제어할 수 없기 때문에 3개의 Random int를 받고 나서도 계속 randStream 채널에는 데이터가 들어오게 된다.
    • 채널은 종료되지 않아서 defer로 호출한 Message가 출력되지 않았다.

변경 후

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
    func main(){
        newRandStream := func(done <-chan struct{}) <-chan int{
            randStream := make(chan int)
            go func(){
                defer fmt.Println("newRandStream closure exited")
                defer close(randStream)
                for {
                    select{
                    case <-done:
                        return
                    case randStream <- rand.Intn(100):
                    }
                }
            }()
            return randStream
        }
        done := make(chan struct{})
        randStream := newRandStream(done)
        for i:=0; i<3; i++{
            fmt.Println(<-randStream)
        }
        <-randStream
        time.Sleep(time.Second)
    }
    /*
    OUTPUT
    81
    87
    47
    newRandStream closure exited
    */ 
  • 여기서도 done 채널을 주어서 부모 고루틴에서 쓰기 채널을 종료할 수 있게 만들어 주었다.

for-select 루프

  • 채널에서 반복된 변수를 보내고자 할 때
1
2
3
4
5
6
7
for _, s := range []string{"a","b","c","d"}{
    select {
    case <-done:
        return
    case stringStream <-s:
    }
}
  • 멈추기를 기다리면서 무한 대기
1
2
3
4
5
6
7
8
for {
    select {
    case <-done:
        return
    default:
        //Wait Infinite
    }
}
This post is licensed under CC BY 4.0 by the author.

-

Kubernetes Custom Controller Workqueue

Comments powered by Disqus.