Backend with Golang

[Concurrency-3] Go Channel의 사용

아직개구리 2023. 6. 13. 19:18
  • unidirectional channel의 경우 function parameter로 사용되는 경우가 많다. 
  • goroutine이 scheduled 되면, exit하는 것에 대한 보장이 되지 않는다. channel을 사용하면, code가 omitted되지 않고 잘 끝날 수 있는데, 이를 통해서 channel이 하는 게 blocking이라고 볼 수 있다. 
  • write가 된다면 그것이 빌때까지 기다린다는 것을 의미한다. 이때 Deadlock 이 잘 일어날 수 있는데, main goroutine에서 value가 placed되길 기다리고 있는데, 이게 일어나지 않으면 모든 goroutine 이 asleep되어 deadlock 이 일어나는 것이다. 
  • value, ok := <- stringStream 이 예시에서 ok 가 뜻하는 것은? 
    • 어떤 곳에서 placed된 value를 읽는 것 or closed channel에서 생성된 default value일 수 있다.
    • 여기서 closed channel이란 더이상 값이 전달되지 않는 채널이다. 보초역할을 하는 것을 직접 만들어서 관리할 수 있지만, 개발자들의 짐을 덜고자 closing 할 수 있는 함수를 제공한다. close(chan) 를 통해서 할 수 있고, closed 되더라도 read 과정을 할 수 있다.  closed channel에 write 를 하면 panic이 된다. 
    • channel은 사실 닫는 과정이 file처럼 필수적이지는 않은데, receiver에게 더이상 받아올 value 가 없음을 알려주기 위해서는 필요하다. 예를 들면 loop을 빠져나와야 할 때처럼. 
  • range chan 이런 형식으로 loop를 돌 수 있는데, 이때 exit condition을 적지 않아도 loop 자체에서 해결해준다. 
  • Buffered channel 은 In-memory FIFO 라고 할 수 있다. 읽히는 지에 관계없이 capacity에 해당하는 갯수의 input 을 넣을 수 있다. 
    • a := make(chan int) 를 buffered channel로 표현하면 a := make(chan int, 0)이 된다. 아무것도 write하지 않아도 이미 full인 상태임을 의미한다. 버퍼가 없다는 것은 receive 하는 고루틴이 재개할 때까지 보내는 고루틴을 중단하고, receive가 먼저 시도 됐다면, 수신하는 고루틴은 다른 고루틴이 송신을 수행할 때까지 중단된다. 
  • Channel이 고루틴을 붙여주는 접착제였다면, switch statement는 channel을 bind해주는 접착제와 같은역할을 한다. 
    • switch block과 비슷하게 생겼지만, 그것과는 다르게 sequentially 실행되지 않는다. 그리고 어떤 기준에도 맞지 않더라도 execution이 실패하진 않는다. 대신에 모든 channel에 읽고 쓰기들이 simultaneously하게 그들이 준비가 되었는지 아닌지 확인된다. (simultaneously 라는 단어는 chapter 6에서 자세히 설명될 것이다. ) 
    • 세가지 question에 대한 답들을 하게 될 것이다. 
      • What happens when multiple channels have something to read? pseudo random하게 실행되어 여러개가 읽히길 기다리고 있을 때는 다 같은 확률로 selected된다고 할 수 있다. 사용자가 channel들을 함께 그룹화 해서 놓은 것의 의도 라던지 problem space를 알 수 없기 때문에, 평균적인 케이스에서 잘 되길 희망하는 것이 Go Runtime이 할 일이다. 모든 channel 이 사용될 확률을 같게 가져가면서 average case에서 잘 작동하도록 하는 것이다. 
      • What if there are never any channels that become ready? 모든 channel 이 모두 블락 되었을 때 쓸모있는 하고자 하는 게 없고, 그렇다고 영원히 block할 수도 없을 때 사용자들은 time out 을 원할 수도 있다. 이 때 Go의 time package를 사용하면 된다. 아래 코드 참조하면 된다. nil 채널일 때 read나 write를 하는 것은 Block을 초래하기 때문에 <-c 의 경우 절대 unblocked가 되지 않는 코드이다. 
      • What if we want to do something but no channels are currently ready? 아무 채널이 ready가 안되어 있을 때 그 빈 시간동안 우리가 무언가 하고 싶다면? 이라는 질문을 맞닥뜨리게 된다. 그럴 때에는 default: 를 사용하게 되는데, 만약 두개의 채널이 있고 둘다 nil 이라면 둘다 unblocked될 일은 없다. 그렇기 때문에 바로 default아래의 있는 구문이 실행되게 될 것이다. default clause는 "goroutine이 다른 고루틴의 결과를 기다리면서 work를 진행하는 경우"에 자주 사용이 된다. 
// 두번째 질문 관련 코드 

var c <-chan int 
select {
case <-c: // unblocked가 되지 않는다. nil 채널에서 읽어오는 것이기 때문이다. 
case <-time.After(1*time.second):
	fmt.Println("Timed out")
}

 

// 세번째 질문 관련 코드 

done := make(chan interface{})
go func() {
	time.Sleep(5*time.second)
    close(done)
}()

workCounter := 0
loop: 
for {
	select {
    case <-done: 
    	break;
    default: 
    }
    //simulate work
    workCounter++
    time.Sleep(1*time.Second)
}

위 코드는 어떤 루프가 있고 그것에서 일을 진행시키는데 때때로 끝내야 하는지 체크하는 코드라고 보면된다. 

 

channel 의 capacity?  여기서 드는 질문:  아래의 코드에서 받아야 하는 result 가 6개인데 buffered channel은 5로 설정해 두는데, 여기서 이걸 하나를 빼고 하는 이유는 무엇인가? 

chanOwner := func() <- chan int {
	resultStream := make(chan int, 5) 
    go func() {
    	defer close(resultStream)
        for i := 0; i <= 5; i ++ {
        	resultStream <- i
        }
    }() 
    return resultStream
}

resultStream := chanOwner()
for result := range resultStream {
	fmt.Println("Received: %d\n", result)
}
fmt.Println("Done receiving!")