[Go] Goroutine - select

윤동환·2023년 7월 4일
1

Go

목록 보기
7/18
post-thumbnail

Select

Goroutine이 다중 커뮤니케이션 연산에서 대기할 수 있게 합니다.

select는 케이스 중 하나가 실행될 수 있을 때까지 block합니다.
그 후 해당 케이스를 실행합니다.
여러 개가 준비되면 무작위로 하나를 선택합니다.

예제

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for { //중지 조건이 없는 while문
		select {
		case c <- x: //채널 c로 보낼 수 있고
			x, y = y, x+y
		case <-quit: //quit에 값이 들어왔을 때 해당 case 실행
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	//2 channel 모두 버퍼링이 없기때문에 
    //누군가 수신을 시도할 때만 채널로 보낼 수 있습니다.
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Print(<-c, " ")
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

결과

코드의 이해

fibonacci(c, quit) 이 함수를 호출하지 않으면 아무런 출력도 되지 않습니다.
func() 내부에 fmt.Print(<-c)가 어떤식으로 인식되어 select되는지 모르겠어서 찾아보았습니다.

slect문의 내용을 해석하면 다음과 같습니다.

select {
case c <- x: // c에 send 할 수 있을 때
    // update my variables
    x, y = y, x+y
case <-quit: // 만약 quit로부터 값을 받을 수 있다면 exit하겠다.
    fmt.Println("quit")
    return
}

default case가 없는 의미
"c에 보낼 수 없고 종료에서 읽을 수 없으면 해당case를 만족할 때 까지 block합니다." 를 뜻합니다.

for i:=0; i<10; i++ {
    fmt.Println(<-c)  // c로 부터 읽는다
}
quit <- 0  // main process를 끝내기 위해 quit에 send한다.

위의 코드가 동작할 수 있는 이유
1. select case를 만족시킬 때까지 block하고 있음
2. goroutine을 사용하여 fibonacci(c, quit)를 분리하여 for 문에서<-c를 실행해켰을 때 fiboancci함수에서 이를 받아 줄 수 있다.

만약 goroutine으로 분리하지 않는다면?
이와 같은 에러를 만나게 됩니다.

위 코드는 producer/consumer(생산자/소비자) 패턴을 사용하고 있으므로 main()과 go func()이 분리되어 동시에 실행되는 것이 중요한 포인트 입니다.

요점 정리와 실행 순서

  1. producer/consumer(생산자/소비자) 패턴을 사용하고 있어서 소비하는 구문인 gfunc()와 생산하는 main() fibonacci를 분리해서(goroutine사용) 작성해야한다.
  2. default 키워드가 없는 select는 case중 하나를 만족할 때 까지 block된다.
  3. buffer channel이 아니기 때문에 <-quit
  4. main문에서 fibonacci함수가 수행되고 이때, x에 0으로 초기화 되며 첫번째 case를 만족하며 select의 block이 풀린다.
  5. 3번이 수행되고나서 x, y는 피보나치 수열을 만족하게 초기화되며 channel c에는 처음 x의 0이 들어가게된다.
  6. c에 0이 들어왔으니 c에서 0을 빼내며 fmt.Println(<-c)를 수행한다.
  7. go func()에서 10번의 반복이 끝나며 quit에 send함으로써 quit에서 값을 꺼낼 수 있게 되었고 case <- quit을 만족하게 된다. 이때 return 구문과 함께 프로세스가 종료된다.

예시 코드 수정

	func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	go fibonacci(c, quit)

위의 코드에서 go 키워드의 위치를 바꾸어 보았다.

결과

fmt.Println구문에서 꺼낼 c값이 없어 에러가 발생하였다.

예시 코드 수정2

	go fibonacci(c, quit)
	 func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()

결과

이렇게 순서를 바꾸어 보니

출력은 되지만 quit가 출력이 되지 않았다
case <-quit되기전 main goroutine인 func()가 종료되며 그런가? 하고 sleep을 주어보았다.

예시 코드 수정3

func main() {
	c := make(chan int)
	quit := make(chan int)
	go fibonacci(c, quit)
	
	 func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0

	}()
	time.Sleep(1)
}

결과

예상대로 quit 출력이 잘 되었다.

예시 코드 수정4

두 함수 모두에 goroutine을 적용하면 어떻게 될까?

	go fibonacci(c, quit)
	
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0

	}()

결과


아무런 출력도 나오지 않았다
예시코드 수정 3처럼 sleep을 걸어주니 그제야 출력이 되었다.
역시 go로 돌려서 main goroutine이 바로 끝나버렸기 때문이다.

reference

  1. https://go-tour-ko.appspot.com/concurrency/5
  2. https://stackoverflow.com/questions/34931059/go-tutorial-select-statement
profile
모르면 공부하고 알게되면 공유하는 개발자

0개의 댓글