공대생 정리노트

동시성 패턴 본문

언어/Go

동시성 패턴

woojinger 2021. 5. 5. 13:47

참고자료

www.yes24.com/Product/Goods/24759320

 

디스커버리 Go 언어

실전에서 쓰는 Go 언어를 익히는 가장 확실한 방법Go는 범용 프로그래밍 언어로, 깔끔하고 간결하게 생산성 높은 프로그래밍이 가능하다. 작성한 코드를 빠르게 컴파일하고 가비지 컬렉션을 지

www.yes24.com


파이프라인 패턴

// PlusOne returns a channel of num + 1 for nums received from in
func PlusOne(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
    	defer close(out)
        for num := range in {
        	out <- num + 1
        }
    }
}

func ExamplePlusOne(){
    c := make(chan int)
    go func() {
    	defer close(c)
        c <- 5
        c <- 3
        c <- 8
    }()

    for num := range PlusOne(PlusOne(c)){
        fmt.Println(num)
    }
}

// Output :
// 7
// 5
// 10

PlusOne은 받기 전용 채널을 받아 다른 받기 전용을 돌려줌.

두 개를 연달아 사용해 2가 더해진 숫자들이 결과로 나온다

type IntPipe func(<-chan int) <- chan int

// 이어진 파이프라인을 하나로 보이게 만드는 패턴
func Chain(ps ...IntPipe) IntPipe {
    return func(in <-chan int) <- chan int{
        c := in
        for _, p := range ps {
            c = p(c)
        }
        return c
    }
}

PlusTwo := Chain(PlusOne, PlusOne)

팬아웃

Fan-out : 게이트 하나의 출력이 게이트 여러 입력으로 들어가는 경우

func main(){
    c := make(chan int)
    for i := 0; i < 3; i++ {
        go func(i int){
            for n := range c {
                time.Sleep(1)
                fmt.Println(i, n)
            }
        }(i)
    }
    for i := 0; i < 10; i++ {
        c <- i
    }
    close(c)
}
        

time.Sleep은 context switch를 강제함. 없으면 하나의 고루틴이 독식하게 된다.

3개의 고루틴을 만들고, 메인에서 0부터 9까지의 수를 채널로 보낸다. 

마지막에 close(c)를 하지 않으면 프로그램이 종료되지 않는 경우에 숫자들을 기다리는 고루틴이 종료되지 않아 계속 메모리에 남아 있게 된다 -> memory leak 발생

고루틴에 i를 사용할 때 i를 따로 넘겨줘서 사용해야함. 그렇게 사용하지 않고 메인에서 바로 받으면 메인 고루틴이 i 값을 계속 증가시키기에 증가된 값을 사용할 수 있음.

팬인

Fan-in : 하나의 게이트에 여러 입력이 들어가는 것

for FanIn(ins ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(ins))
    for _, in := range ins {
        go func(in <-chan int){
            defer wg.Done()
            for num := range in {
                out <- num
            }
        }(in)
    }
    go func(){
        wg.Wait()
        close(out)
    }()
    return out
}

..
// 사용 예제
// c1, c2, c3 채널에서 나온 자료들은 c로 나옴
c := FanIn(c1, c2, c3)

분산처리

//팬아웃 + 파이프라인 + 팬인
func Distribute(p IntPipe, n int) IntPipe{
   return func(in <-chan int) <-chan int {
       cs := make([]<-chan int, n)
       for i := 0; i < n; i++ {
           cs[i] = p(in)
       }
       return FanIn(cs...)
   }
}

IntPipe 형태의 함수를 받은 뒤 n 개로 분산처리 하는 함수로 돌려주는 함수

Select

select의 특징

  • 모든 case가 계산된다. 
  • 각 case는 채널에 입출력하는 형태가 되며 막히지 않고 입출력이 가능한 case가 있으면 그중에 하나가 선택되어 입출력을 수행한다
  • default가 있으면 모든 case에 입출력이 불가능할 때 코드 수행. default가 없고 모든 case가 입출력 불가면 하나라도 가능할 때까지 기다린다

팬인

select {
    case n := <-c1: c <- n
    case n := <-c2: c <- n
    case n := <-c3: c <- n
}

위 select문을 for 반복문으로 둘러싸면 fan in

채널이 닫히면 막히지 않고 기본값을 계속 받아갈 수 있기에 위 경우 닫힌 채널의 case를 선택할 수 있음

닫힌 채널은 nil로 바꿔주어 영원히 막힌 채널로 바꿔줘야 한다

채널을 기다리지 않고 받기

select {
    case n:= <-c:
        fmt.Println(n)
    default:
        fmt.Println("skip")
}

시간제한

timeout := time.After(5 * time.Second)
for {
    select {
        case n := <- recv:
            fmt.Println(n)
        case send <- 1:
            fmt.Println("sent 1")
        case <-timeout
            fmt.Println("not finished in 5 sec")
            return
    }
}

파이프라인 중단하기

받는 쪽에서 채널을 닫아버리면 닫힌 채널에 자료를 보낼 때 패닉 발생

이를 해결하는 패턴은 done 채널을 하나 더 두는 것

보내는 고루틴에서 채널로부터 신호가 감지되면 보내는 것을 중단하게 만든다( close(done)으로 broadcast)

컨텍스트(context.Context) 활용

go get golang.org/x/net/context

done 채널 패턴 이용 대신 context 라이브러리 활용 가능

주의점

// WARNING! This is a bad example
c := make(chan int)
done := make(chan bool)
go func() {
    for i := 0; i < 10; i++ {
        c <- i
    }
    done <- true
}()
go func() {
    for {
        fmt.Println(<-c)
    }
}()
<-done

위 코드는 문제점들이 여럿 있다

  • 두번째 고루틴이 끝나지 않는다. 위 코드가 반복 수행될 경우 고루틴의 수는 점점 늘어난다
  • 첫 번째 고루틴은 생산이 끝난 이후 done에 true를 넣어준다. 메인 고루틴은 <-done으로 끝날 때까지 기다린다. done은 생산이 끝난 뒤에 값이 들어가므로 소비가 끝나기전에 메인 고루틴이 끝날 가능성이 생긴다. fmt.Println(<-c)가 한줄이어도 <-c로 받아와서 출력을 할 때 context switch가 일어날 수 있는 것

버그를 막기 위한 주의점

  • 자료를 보내는 채널은 보내는 쪽에서 닫는다
  • 닫을 때 defer을 이용하는 것이 좋다. 사용하지 않으면 중간에 return 했을 때 채널 안닫는 경우가 생길 수 있다
  • 받는 쪽이 끝날때까지 기다리는 것이 안정적이다. 위 예제에서는 소비자 쪽에서 done<- true를 했어야 했다. 소비자가 끝난 것을 알려면 close로 신호를 주어야 한다
  • 받는 쪽에서는 range를 이용하는 것이 생산자가 채널을 닫으면 빠져나오기 때문에 편리하다
  • 루틴이 끝났음을 알리고 다른쪽에서 기다리는 것은 sync.WaitGroup을 이용하는 것이 낫다
  • 끝났음을 알리는 done 채널은 보내는 쪽에서 결정하는 사항이 아니다. 보내는 쪽은 채널을 닫아서 알리는 것이 낫고, done 채널은 받는 쪽에서 더 이상 자료를 보내지 말아달라는 cancel 요청으로 보는 것이 낫다
  • done 채널에 자료를 보내 신호를 보내는 것 보다 close(done)으로 채널을 닫는 것이 좋다

 

'언어 > Go' 카테고리의 다른 글

Effective Go (2) 요약  (0) 2021.11.01
Effective Go (1) 요약  (0) 2021.10.18
고루틴  (0) 2021.05.03
Server 함수 정리  (0) 2020.12.29
Test시 Cannot import "main"이 뜨며 build fail  (0) 2020.12.20
Comments